博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
随便聊聊水面效果的2D实现(二)
阅读量:4225 次
发布时间:2019-05-26

本文共 32674 字,大约阅读时间需要 108 分钟。

0. 引子

  提到想要随便聊一聊的2D实现方法,近来又总算有了些许空余时间,于是便有了这篇东西~

 

1. 概述

 

  RippleEffect我个人的理解是波纹或者说涟漪效果,与之前所讲的WaterEffect有所不同的是,RippleEffect表现的是水波产生与消散的一个过程,而WaterEffect更注重的则是持续的水波“荡漾”效果。

  

  其实游戏中的Ripple效果也很常见,譬如在之前提到过的《Crysis》中,波纹效果就被应用到了很多地方(射击水面等等)

 

  

 

  在3D游戏中,波纹效果的实现方式大概仍然是先将水面进行网格划分,然后根据波纹初始形状改变顶点位置,最后辅以一定的波纹传播及消散过程。

  Cocos2d-x中其实也有一个类似的效果,有兴趣的朋友可以仔细看看~

 

2. 方法

 

  OK,闲话少叙,还是让我们来看看2D实现Ripple效果的几种方法~

 

  # 使用Shader

 

  如果看过的朋友一定了解,在实现2DWater效果时,我多次使用了,而对于Ripple效果,我们同样可以借助FS的力量:

 

  首先我们需要定义一个RippleEffectSprite类型,相关代码比较简易,在此完整列出:

 

// RippleEffectSprite.h#ifndef __RIPPLE_EFFECT_SPRITE_H__#define __RIPPLE_EFFECT_SPRITE_H__#include "cocos2d.h"USING_NS_CC;class RippleEffectSprite : public Sprite {public:	static RippleEffectSprite* create(const char* pszFileName);public:	bool initWithTexture(Texture2D* texture, const Rect& rect);	void initGLProgram();private:	virtual void update(float delta) override;	void updateRippleParams();private:	float m_rippleDistance{ 0 };	float m_rippleRange{ 0.02 };};#endif
// RippleEffectSprite.cpp#include "RippleEffectSprite.h"RippleEffectSprite* RippleEffectSprite::create(const char* pszFileName) {	auto pRet = new (std::nothrow) RippleEffectSprite();	if (pRet && pRet->initWithFile(pszFileName)) {		pRet->autorelease();	}	else {		CC_SAFE_DELETE(pRet);	}	return pRet;}bool RippleEffectSprite::initWithTexture(Texture2D* texture, const Rect& rect) {	if (Sprite::initWithTexture(texture, rect)) {#if CC_ENABLE_CACHE_TEXTURE_DATA		auto listener = EventListenerCustom::create(EVENT_RENDERER_RECREATED, [this](EventCustom* event) {			setGLProgram(nullptr);			initGLProgram();		});		_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);#endif		initGLProgram();		return true;	}	return false;}	void RippleEffectSprite::initGLProgram() {	auto fragSource = (GLchar*)String::createWithContentsOfFile(		FileUtils::getInstance()->fullPathForFilename("Shaders/RippleEffect.fsh").c_str())->getCString();	auto program = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, fragSource);	auto glProgramState = GLProgramState::getOrCreateWithGLProgram(program);	setGLProgramState(glProgramState);	updateRippleParams();	// NOTE: now we need schedule update here	scheduleUpdate();}void RippleEffectSprite::update(float delta) {	updateRippleParams();	// TODO: improve	float rippleSpeed = 0.25f;	float maxRippleDistance = 1;	m_rippleDistance += rippleSpeed * delta;	m_rippleRange = (1 - m_rippleDistance / maxRippleDistance) * 0.02f;	if (m_rippleDistance > maxRippleDistance) {		updateRippleParams();		unscheduleUpdate();	}}void RippleEffectSprite::updateRippleParams() {	getGLProgramState()->setUniformFloat("u_rippleDistance", m_rippleDistance);	getGLProgramState()->setUniformFloat("u_rippleRange", m_rippleRange);}

  上述代码除了不断更新设置FS中的两个uniform变量(u_rippleDistanceu_rippleRange)之外,其他并无特殊之处~

 

  接着让我们看看实际的Fragment Shader

varying vec4 v_fragmentColor; varying vec2 v_texCoord;uniform float u_rippleDistance;uniform float u_rippleRange;float waveHeight(vec2 p) {	float ampFactor = 2;	float distFactor = 2;	float dist = length(p);	float delta = abs(u_rippleDistance - dist);	if (delta <= u_rippleRange) {	    return cos((u_rippleDistance - dist) * distFactor) * (u_rippleRange - delta) * ampFactor;	}    else {	    return 0;	}}void main() {    vec2 p = v_texCoord - vec2(0.5, 0.5);    vec2 normal = normalize(p);	// offset texcoord along dist direction    v_texCoord += normal * waveHeight(p);	    gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor;}

  原理上来说,FS根据当前“片段”离(波纹)中心的距离来计算相应的“片段”高度(当不在波纹中时高度便为0),然后根据计算所得的高度值来偏移像素,基本就是这样~

 

  依然给张截图:)

 

 

 

  # 网格划分

 

  其实在2D中我们也可以进行网格划分,只是在模拟波纹的过程中,我们并不改变网格顶点的位置,而是改变相应顶点的纹理坐标。

  

  实现方式依然是正弦余弦函数的运用,波纹传递和衰减的模拟亦不可少,下面贴出的代码其实最早应该来源于,不过由于年代久远,代码仍然是基于Cocos2d 1.x版本编写的,后来也有不少朋友进行了移植和改写,有兴趣的朋友可以google一下,这里给出的则是自己基于Cocos2d-x 3.x改写的版本,在此完整列出,原代码其实细节很多,但注释完善,非常值得一读~

// pgeRippleSprite.h#ifndef __PGE_RIPPLE_SPRITE_H__#define __PGE_RIPPLE_SPRITE_H__#include 
#include "cocos2d.h"USING_NS_CC;// --------------------------------------------------------------------------// defines#define RIPPLE_DEFAULT_QUAD_COUNT_X 32 #define RIPPLE_DEFAULT_QUAD_COUNT_Y 16 #define RIPPLE_BASE_GAIN 0.1f // an internal constant#define RIPPLE_DEFAULT_RADIUS 500 // radius in pixels #define RIPPLE_DEFAULT_RIPPLE_CYCLE 0.25f // timing on ripple ( 1/frequency )#define RIPPLE_DEFAULT_LIFESPAN 3.6f // entire ripple lifespan#define RIPPLE_CHILD_MODIFIER 2.0f// --------------------------------------------------------------------------// typedefsenum class RippleType{ Rubber, // a soft rubber sheet Gel, // high viscosity fluid Water // low viscosity fluid};enum class RippleChildType{ Left, Top, Right, Bottom};struct RippleData{ bool parent; // ripple is a parent bool childCreated[4]; // child created ( in the 4 direction ) RippleType rippleType; // type of ripple ( se update: ) Vec2 center; // ripple center ( but you just knew that, didn't you? ) Vec2 centerCoordinate; // ripple center in texture coordinates float radius; // radius at which ripple has faded 100% float strength; // ripple strength float runtime; // current run time float currentRadius; // current radius float rippleCycle; // ripple cycle timing float lifespan; // total life span };// --------------------------------------------------------------------------// pgeRippleSpriteclass pgeRippleSprite : public Node{public: pgeRippleSprite(); virtual ~pgeRippleSprite(); void reset() { clearRipples(); }public: static pgeRippleSprite* create(const char* filename); static pgeRippleSprite* create(Texture2D* texture); bool initWithFile(const char* filename); bool initWithTexture(Texture2D* texture); virtual void draw(Renderer *renderer, const Mat4& transform, uint32_t flags) override; void onDraw(const Mat4& transform, uint32_t flags); virtual void update(float dt); void addRipple(const Vec2& pos, RippleType type, float strength); bool getInverse() const { return m_inverse; } void setInverse(bool inverse);protected: bool m_inverse; // inverse flagprotected: void tesselate(); void addRippleChild(RippleData* parent, RippleChildType type); void clearRipples();protected: CC_SYNTHESIZE(Texture2D*, m_texture, Texture) CC_SYNTHESIZE(int, m_quadCountX, QuadCountX) CC_SYNTHESIZE(int, m_quadCountY, QuadCountY) CC_SYNTHESIZE(int, m_VerticesPrStrip, VerticesPrStrip) CC_SYNTHESIZE(int, m_bufferSize, BuffSize) CC_SYNTHESIZE(Vec2*, m_vertice, Vertice) CC_SYNTHESIZE(Vec2*, m_textureCoordinate, TextureCoordinate) CC_SYNTHESIZE(Vec2*, m_rippleCoordinate, RippleCoordinate) CC_SYNTHESIZE_READONLY(bool*, m_edgeVertice, EdgeVertice) CC_SYNTHESIZE_READONLY_PASS_BY_REF(std::list
, m_rippleList, RippleList)protected: // render command CustomCommand m_customCommand;};#endif
// pgeRippleSprite.cpp#include "pgeRippleSprite.h"pgeRippleSprite* pgeRippleSprite::create(const char* filename){	auto sprite = new (std::nothrow) pgeRippleSprite();	if (sprite && sprite->initWithFile(filename))	{		sprite->autorelease();		return sprite;	}	CC_SAFE_DELETE(sprite);	return NULL;}pgeRippleSprite* pgeRippleSprite::create(CCTexture2D* texture){	auto sprite = new (std::nothrow) pgeRippleSprite();	if (sprite && sprite->initWithTexture(texture))	{		sprite->autorelease();		return sprite;	}	CC_SAFE_DELETE(sprite);	return NULL;}pgeRippleSprite::pgeRippleSprite()	:m_texture(NULL),	m_vertice(NULL),	m_textureCoordinate(NULL),	m_rippleCoordinate(NULL),	m_edgeVertice(NULL){}pgeRippleSprite::~pgeRippleSprite(){	CC_SAFE_RELEASE(m_texture);	CC_SAFE_DELETE_ARRAY(m_vertice);	CC_SAFE_DELETE_ARRAY(m_textureCoordinate);	CC_SAFE_DELETE_ARRAY(m_rippleCoordinate);	CC_SAFE_DELETE_ARRAY(m_edgeVertice);	clearRipples();}bool pgeRippleSprite::initWithFile(const char* filename){	return initWithTexture(CCTextureCache::sharedTextureCache()->addImage(filename));}bool pgeRippleSprite::initWithTexture(CCTexture2D* texture){	m_texture = texture;	if (!m_texture) return false;	m_texture->retain();	m_vertice = NULL;	m_textureCoordinate = NULL;	CC_SAFE_DELETE_ARRAY(m_vertice);	CC_SAFE_DELETE_ARRAY(m_textureCoordinate);	CC_SAFE_DELETE_ARRAY(m_rippleCoordinate);	CC_SAFE_DELETE_ARRAY(m_edgeVertice);	m_quadCountX = RIPPLE_DEFAULT_QUAD_COUNT_X;	m_quadCountY = RIPPLE_DEFAULT_QUAD_COUNT_Y;	m_inverse = false;	tesselate();	scheduleUpdate();	setContentSize(m_texture->getContentSize());	//setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture));	setGLProgram(ShaderCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE));	return true;}void pgeRippleSprite::onDraw(const Mat4& transform, uint32_t flags){	getGLProgram()->use();	getGLProgram()->setUniformsForBuiltins(transform);	GL::bindTexture2D(m_texture->getName());	GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POSITION | GL::VERTEX_ATTRIB_FLAG_TEX_COORD);	// TODO: use VBO or even VAO	glBindBuffer(GL_ARRAY_BUFFER, 0);	float* vertexBuffer = NULL;	float* coordBuffer = NULL;	CCPoint* coordSource = (m_rippleList.size() == 0) ? m_textureCoordinate : m_rippleCoordinate;	if (sizeof(CCPoint) == sizeof(ccVertex2F))	{		vertexBuffer = (float*)m_vertice;		coordBuffer = (float*)coordSource;	}	else	{		// NOTE: clear these soon		static float* s_vertexBuffer = new float[2 * m_VerticesPrStrip * m_quadCountY];		static float* s_coordBuffer = new float[2 * m_VerticesPrStrip * m_quadCountY];		for (int i = 0; i < m_VerticesPrStrip * m_quadCountY; ++i)		{			s_vertexBuffer[i * 2] = m_vertice[i].x;			s_vertexBuffer[i * 2 + 1] = m_vertice[i].y;			s_coordBuffer[i * 2] = coordSource[i].x;			s_coordBuffer[i * 2 + 1] = coordSource[i].y;		}		vertexBuffer = s_vertexBuffer;		coordBuffer = s_coordBuffer;	}	glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertexBuffer);	glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, coordBuffer);	for (int strip = 0; strip < m_quadCountY; ++strip)	{		glDrawArrays(GL_TRIANGLE_STRIP, strip * m_VerticesPrStrip, m_VerticesPrStrip);	}}void pgeRippleSprite::clearRipples(){	auto iterBegin = m_rippleList.begin();	while (iterBegin != m_rippleList.end())	{		RippleData* date = *iterBegin;		CC_SAFE_DELETE(date);		iterBegin++;	}	m_rippleList.clear();}void pgeRippleSprite::tesselate(){	CC_SAFE_DELETE_ARRAY(m_vertice);	CC_SAFE_DELETE_ARRAY(m_textureCoordinate);	CC_SAFE_DELETE_ARRAY(m_rippleCoordinate);	CC_SAFE_DELETE_ARRAY(m_edgeVertice);	m_VerticesPrStrip = 2 * (m_quadCountX + 1);	m_bufferSize = m_VerticesPrStrip * m_quadCountY;	//allocate buffers	m_vertice = new CCPoint[m_bufferSize];	m_textureCoordinate = new CCPoint[m_bufferSize];	m_rippleCoordinate = new CCPoint[m_bufferSize];	m_edgeVertice = new bool[m_bufferSize];	int vertexPos = 0;	CCPoint normalized;	CCSize contentSize = m_texture->getContentSize();	for (int y = 0; y < m_quadCountY; ++y)	{		for (int x = 0; x < (m_quadCountX + 1); ++x)		{			for (int yy = 0; yy < 2; ++yy)			{				// first simply calculate a normalized position into rectangle				normalized.x = (float)x / (float)m_quadCountX;				normalized.y = (float)(y + yy) / (float)m_quadCountY;				// calculate vertex by multiplying rectangle ( texture ) size				m_vertice[vertexPos] = ccp(normalized.x * contentSize.width, normalized.y * contentSize.height);				// adjust texture coordinates according to texture size				// as a texture is always in the power of 2, maxS and maxT are the fragment of the size actually used				// invert y on texture coordinates				m_textureCoordinate[vertexPos] = ccp(normalized.x * m_texture->getMaxS(), m_texture->getMaxT() - (normalized.y * m_texture->getMaxT()));				// check if vertice is an edge vertice, because edge vertices are never modified to keep outline consistent				m_edgeVertice[vertexPos] = (					(x == 0) ||					(x == m_quadCountX) ||					((y == 0) && (yy == 0)) ||					((y == (m_quadCountY - 1)) && (yy > 0)));				// next buffer pos				++vertexPos;			}		}	}}void pgeRippleSprite::addRipple(const cocos2d::CCPoint &pos, RippleType type, float strength){	// allocate new ripple	RippleData* newRipple = new RippleData();	// initialize ripple	newRipple->parent = true;	for (int count = 0; count < 4; ++count)	{		newRipple->childCreated[count] = false;	}	newRipple->rippleType = type;	newRipple->center = pos;	CCSize contentSize = m_texture->getContentSize();	newRipple->centerCoordinate = ccp(pos.x / contentSize.width * m_texture->getMaxS(), m_texture->getMaxT() - (pos.y / contentSize.height * m_texture->getMaxT()));	newRipple->radius = RIPPLE_DEFAULT_RADIUS;	newRipple->strength = strength;	newRipple->runtime = 0;	newRipple->currentRadius = 0;	newRipple->rippleCycle = RIPPLE_DEFAULT_RIPPLE_CYCLE;	newRipple->lifespan = RIPPLE_DEFAULT_LIFESPAN;	// add ripple to running list 	m_rippleList.push_back(newRipple);}void pgeRippleSprite::addRippleChild(RippleData* parent, RippleChildType type){	// allocate new ripple	RippleData* newRipple = new RippleData();	CCPoint pos;	// new ripple is pretty much a copy of its parent	memcpy(newRipple, parent, sizeof(RippleData));	// not a parent	newRipple->parent = false;	CCSize winSize = CCDirector::sharedDirector()->getWinSize();	// mirror position	switch (type) {	case RippleChildType::Left:		pos = ccp(-parent->center.x, parent->center.y);		break;	case RippleChildType::Top:		pos = ccp(parent->center.x, winSize.height + (winSize.height - parent->center.y));		break;	case RippleChildType::Right:		pos = ccp(winSize.width + (winSize.width - parent->center.x), parent->center.y);		break;	case RippleChildType::Bottom:	default:		pos = ccp(parent->center.x, -parent->center.y);		break;	}	newRipple->center = pos;	CCSize contentSize = m_texture->getContentSize();	newRipple->centerCoordinate = ccp(pos.x / contentSize.width * m_texture->getMaxS(), m_texture->getMaxT() - (pos.y / contentSize.height * m_texture->getMaxT()));	newRipple->strength *= RIPPLE_CHILD_MODIFIER;	// indicate child used	parent->childCreated[(unsigned)type] = true;	// add ripple to running list 	m_rippleList.push_back(newRipple);}void pgeRippleSprite::update(float dt){	// test if any ripples at all	if (m_rippleList.size() == 0) return;	RippleData* ripple;	CCPoint pos;	float distance, correction;	// ripples are simulated by altering texture coordinates	// on all updates, an entire new array is calculated from the base array 	// not maintaining an original set of texture coordinates, could result in accumulated errors	memcpy(m_rippleCoordinate, m_textureCoordinate, m_bufferSize * sizeof(CCPoint));	// scan through running ripples	// the scan is backwards, so that ripples can be removed on the fly	CCSize winSize = CCDirector::sharedDirector()->getWinSize();	auto iterRipple = m_rippleList.rbegin();	while (iterRipple != m_rippleList.rend())	{		// get ripple data		ripple = *iterRipple;		// scan through all texture coordinates		for (int count = 0; count < m_bufferSize; ++count)		{			// don't modify edge vertices			if (!m_edgeVertice[count])			{				// calculate distance				// you might think it would be faster to do a box check first				// but it really isn't, 				// ccpDistance is like my sexlife - BAM! - and its all over				distance = ccpDistance(ripple->center, m_vertice[count]);				// only modify vertices within range				if (distance <= ripple->currentRadius)				{					// load the texture coordinate into an easy to use var					pos = m_rippleCoordinate[count];					// calculate a ripple 					switch (ripple->rippleType)					{					case RippleType::Rubber:						// method A						// calculate a sinus, based only on time						// this will make the ripples look like poking a soft rubber sheet, since sinus position is fixed						correction = sinf(2 * M_PI * ripple->runtime / ripple->rippleCycle);						break;					case RippleType::Gel:						// method B						// calculate a sinus, based both on time and distance						// this will look more like a high viscosity fluid, since sinus will travel with radius						correction = sinf(2 * M_PI * (ripple->currentRadius - distance) / ripple->radius * ripple->lifespan / ripple->rippleCycle);						break;					case RippleType::Water:					default:						// method c						// like method b, but faded for time and distance to center						// this will look more like a low viscosity fluid, like water     						correction = (ripple->radius * ripple->rippleCycle / ripple->lifespan) / (ripple->currentRadius - distance);						if (correction > 1.0f) correction = 1.0f;						// fade center of quicker						correction *= correction;						correction *= sinf(2 * M_PI * (ripple->currentRadius - distance) / ripple->radius * ripple->lifespan / ripple->rippleCycle);						break;					}					// fade with distance					correction *= 1 - (distance / ripple->currentRadius);					// fade with time					correction *= 1 - (ripple->runtime / ripple->lifespan);					// adjust for base gain and user strength					correction *= RIPPLE_BASE_GAIN;					correction *= ripple->strength;					// finally modify the coordinate by interpolating					// because of interpolation, adjustment for distance is needed, 					correction /= ccpDistance(ripple->centerCoordinate, pos);					pos = ccpAdd(pos, ccpMult(ccpSub(pos, ripple->centerCoordinate), correction));					// another approach for applying correction, would be to calculate slope from center to pos					// and then adjust based on this					// clamp texture coordinates to avoid artifacts					pos = ccpClamp(pos, Vec2::ZERO, ccp(m_texture->getMaxS(), m_texture->getMaxT()));					// save modified coordinate					m_rippleCoordinate[count] = pos;				}			}		}		// calculate radius		ripple->currentRadius = ripple->radius * ripple->runtime / ripple->lifespan;		// check if ripple should expire		ripple->runtime += dt;		if (ripple->runtime >= ripple->lifespan)		{			// free memory, and remove from list			CC_SAFE_DELETE(ripple);			auto it = --iterRipple.base();			auto it_after_del = m_rippleList.erase(it);			iterRipple = std::list
::reverse_iterator(it_after_del); } else { // check for creation of child ripples // NOTE: now we do not need this /* if (ripple->parent == true) { // left ripple if ((ripple->childCreated[(unsigned)RippleChildType::Left] == false) && (ripple->currentRadius > ripple->center.x)) { addRippleChild(ripple, RippleChildType::Left); } // top ripple if ((ripple->childCreated[(unsigned)RippleChildType::Top] == false) && (ripple->currentRadius > winSize.height - ripple->center.y)) { addRippleChild(ripple, RippleChildType::Top); } // right ripple if ((ripple->childCreated[(unsigned)RippleChildType::Right] == false) && (ripple->currentRadius > winSize.width - ripple->center.x)) { addRippleChild(ripple, RippleChildType::Right); } // bottom ripple if ((ripple->childCreated[(unsigned)RippleChildType::Bottom] == false) && (ripple->currentRadius > ripple->center.y)) { addRippleChild(ripple, RippleChildType::Bottom); } } */ iterRipple++; } }}void pgeRippleSprite::setInverse(bool inverse){ if (inverse != m_inverse) { m_inverse = inverse; for (int i = 0; i < m_VerticesPrStrip * m_quadCountY; ++i) { m_textureCoordinate[i].y = 1.0f - m_textureCoordinate[i].y; } }}void pgeRippleSprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) { m_customCommand.init(_globalZOrder); m_customCommand.func = CC_CALLBACK_0(pgeRippleSprite::onDraw, this, transform, flags); renderer->addCommand(&m_customCommand);}

  仍旧给张截图~

  

 

  # 物理模拟

 

  目前个人感觉效果最好的波纹实现方式,当然,这里所谓的物理只是简单的模拟了水波传递和消减的过程,与什么流体动力学没有多大关系,但即便如此,效果感觉也是非常真实的,毕竟其实现方式遵循了一定的物理原则,而我们人类感知的基础其实也就是这种种物理法则罢了,另外,这种实现方式还有一个极大的好处,就是其不存在波纹数量的限制,而上面提到的两种方式都没有这个优点,一旦波纹数量增多,效率的损失就非常明显~

  

  相关的原理说明,网上已有了非常好的教程(,也有一个挺有意思的相关解说),以下列出的代码其实大部分参照了苹果的一个Sample),有兴趣的朋友可以仔细看看:

// PhysicsRippleSprite.h#ifndef __PHYSICS_RIPPLE_SPRITE_H__#define __PHYSICS_RIPPLE_SPRITE_H__#include using std::map;#include "cocos2d.h"USING_NS_CC;struct PhysicsRippleSpriteConfig {	int quadCountX{ 16 };	int quadCountY{ 10 };	int touchRadius{ 5 };	float updateInterval{ 1 / 30.0f };	PhysicsRippleSpriteConfig() {	}	PhysicsRippleSpriteConfig(int countX, int countY, int radius, float interval) :		quadCountX(countX),		quadCountY(countY),		touchRadius(radius),		updateInterval(interval) {	}};class PhysicsRippleSprite : public CCNode {public:	// TODO: improve	static PhysicsRippleSprite* create(const char* filename, const PhysicsRippleSpriteConfig& config = PhysicsRippleSpriteConfig());	static PhysicsRippleSprite* create(CCTexture2D* texture, const PhysicsRippleSpriteConfig& config = PhysicsRippleSpriteConfig());public:	virtual ~PhysicsRippleSprite();	bool init(const char* filename, const PhysicsRippleSpriteConfig& config);	bool init(CCTexture2D* texture, const PhysicsRippleSpriteConfig& config);	void reset();	virtual void draw(Renderer *renderer, const Mat4& transform, uint32_t flags) override;	void onDraw(const Mat4& transform);	virtual void update(float deltaTime) override;	void addRipple(const CCPoint& pos, float strength);private:	void initRippleBuffer();	void initRippleCoeff();	void initRippleMesh();	void generateRippleCoeff(int touchRadius);private:	PhysicsRippleSpriteConfig m_config;private:	CCTexture2D* m_texture{ nullptr };	int m_bufferSize{ 0 };	CCPoint* m_vertices{ nullptr };	CCPoint* m_texCoords{ nullptr };private:	//float* m_rippleCoeff{ nullptr };	map
m_rippleCoeffs; float* m_rippleSource{ nullptr }; float* m_rippleDest{ nullptr };private: float m_elapseTime{ 0 };private: CustomCommand m_customCommand;};#endif // __PHYSICS_RIPPLE_SPRITE_H__

// PhysicsRippleSprite.cpp#include "PhysicsRippleSprite.h"#include 
PhysicsRippleSprite*PhysicsRippleSprite::create(const char* filename, const PhysicsRippleSpriteConfig& config) { auto rippleSprite = new PhysicsRippleSprite(); if (rippleSprite && rippleSprite->init(filename, config)) { rippleSprite->autorelease(); return rippleSprite; } else { CC_SAFE_DELETE(rippleSprite); return nullptr; }}PhysicsRippleSprite*PhysicsRippleSprite::create(CCTexture2D* texture, const PhysicsRippleSpriteConfig& config) { auto rippleSprite = new PhysicsRippleSprite(); if (rippleSprite && rippleSprite->init(texture, config)) { rippleSprite->autorelease(); return rippleSprite; } else { CC_SAFE_DELETE(rippleSprite); return nullptr; }}PhysicsRippleSprite::~PhysicsRippleSprite() { CC_SAFE_RELEASE(m_texture); CC_SAFE_DELETE_ARRAY(m_vertices); CC_SAFE_DELETE_ARRAY(m_texCoords); for (auto kv : m_rippleCoeffs) { CC_SAFE_DELETE_ARRAY(kv.second); } CC_SAFE_DELETE_ARRAY(m_rippleSource); CC_SAFE_DELETE_ARRAY(m_rippleDest);}bool PhysicsRippleSprite::init(const char* filename, const PhysicsRippleSpriteConfig& config) { auto texture = CCTextureCache::sharedTextureCache()->addImage(filename); return init(texture, config);}bool PhysicsRippleSprite::init(CCTexture2D* texture, const PhysicsRippleSpriteConfig& config) { if (!texture) { return false; } m_texture = texture; m_texture->retain(); m_config = config; initRippleBuffer(); initRippleCoeff(); initRippleMesh(); setContentSize(m_texture->getContentSize()); setGLProgram(ShaderCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE)); //setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture)); scheduleUpdate(); return true;}void PhysicsRippleSprite::reset() { // now we just reset ripple height data memset(m_rippleSource, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)); memset(m_rippleDest, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)); // reset elapse time m_elapseTime = 0;}void PhysicsRippleSprite::initRippleBuffer() { m_rippleSource = new float[(m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)]; m_rippleDest = new float[(m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)]; // +2 for padding the border memset(m_rippleSource, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)); memset(m_rippleDest, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float));}void PhysicsRippleSprite::initRippleCoeff() { generateRippleCoeff(m_config.touchRadius);}// TODO: improvevoid PhysicsRippleSprite::generateRippleCoeff(int touchRadius) { if (m_rippleCoeffs.find(touchRadius) == m_rippleCoeffs.end()) { auto rippleCoeff = new float[(touchRadius * 2 + 1) * (touchRadius * 2 + 1) * sizeof(float)]; for (int y = 0; y <= 2 * touchRadius; ++y) { for (int x = 0; x <= 2 * touchRadius; ++x) { float distance = sqrt((x - touchRadius) * (x - touchRadius) + (y - touchRadius) * (y - touchRadius)); if (distance <= touchRadius) { float factor = distance / touchRadius; // goes from -512 -> 0 rippleCoeff[y * (touchRadius * 2 + 1) + x] = -(cos(factor * M_PI) + 1.0f) * 256.0f; } else { rippleCoeff[y * (touchRadius * 2 + 1) + x] = 0.0f; } } } // buffer it m_rippleCoeffs[touchRadius] = rippleCoeff; }}void PhysicsRippleSprite::initRippleMesh() { // NOTE: not so sure about this ... /* for (int i = 0; i < m_config.quadCountY; ++i) { for (int j = 0; j < m_config.quadCountX; ++j) { rippleVertices[(i*poolWidth + j) * 2 + 0] = -1.f + j*(2.f / (poolWidth - 1)); rippleVertices[(i*poolWidth + j) * 2 + 1] = 1.f - i*(2.f / (poolHeight - 1)); rippleTexCoords[(i*poolWidth + j) * 2 + 0] = (float)i / (poolHeight - 1) * texCoordFactorS + texCoordOffsetS; rippleTexCoords[(i*poolWidth + j) * 2 + 1] = (1.f - (float)j / (poolWidth - 1)) * texCoordFactorT + texCoordFactorT; } } */ int verticesPerStrip = 2 * (m_config.quadCountX + 1); m_bufferSize = verticesPerStrip * m_config.quadCountY; m_vertices = new CCPoint[m_bufferSize]; m_texCoords = new CCPoint[m_bufferSize]; CCSize textureSize = m_texture->getContentSize(); CCPoint normalized; int index = 0; for (int y = 0; y < m_config.quadCountY; ++y) { for (int x = 0; x < (m_config.quadCountX + 1); ++x) { for (int z = 0; z < 2; ++z) { // first calculate a normalized position into rectangle normalized.x = (float)x / (float)m_config.quadCountX; normalized.y = (float)(y + z) / (float)m_config.quadCountY; // calculate vertex by multiplying texture size m_vertices[index] = ccp(normalized.x * textureSize.width, normalized.y * textureSize.height); // adjust texture coordinates according to texture size // as a texture is always in the power of 2, maxS and maxT are the fragment of the size actually used // invert y on texture coordinates m_texCoords[index] = ccp(normalized.x * m_texture->getMaxS(), m_texture->getMaxT() - (normalized.y * m_texture->getMaxT())); // next index ++index; } } }}// TODO: improvevoid PhysicsRippleSprite::onDraw(const Mat4& transform) { getGLProgram()->use(); getGLProgram()->setUniformsForBuiltins(transform); GL::bindTexture2D(m_texture->getName()); GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POSITION | GL::VERTEX_ATTRIB_FLAG_TEX_COORD); // TODO: use VBO or even VAO glBindBuffer(GL_ARRAY_BUFFER, 0); CCAssert(sizeof(CCPoint) == sizeof(ccVertex2F), "Incorrect ripple sprite buffer format"); glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, m_vertices); glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, m_texCoords); int verticesPerStrip = m_bufferSize / m_config.quadCountY; for (int i = 0; i < m_config.quadCountY; ++i) { glDrawArrays(GL_TRIANGLE_STRIP, i * verticesPerStrip, verticesPerStrip); }}void PhysicsRippleSprite::update(float deltaTime) { m_elapseTime += deltaTime; if (m_elapseTime < m_config.updateInterval) { return; } else { m_elapseTime -= int(m_elapseTime / m_config.updateInterval) * m_config.updateInterval; } for (int y = 0; y < m_config.quadCountY; ++y) { for (int x = 0; x < m_config.quadCountX; ++x) { // * - denotes current pixel // // a // c * d // b // +1 to both x/y values because the border is padded float a = m_rippleSource[(y)* (m_config.quadCountX + 2) + x + 1]; float b = m_rippleSource[(y + 2) * (m_config.quadCountX + 2) + x + 1]; float c = m_rippleSource[(y + 1) * (m_config.quadCountX + 2) + x]; float d = m_rippleSource[(y + 1) * (m_config.quadCountX + 2) + x + 2]; float result = (a + b + c + d) / 2.f - m_rippleDest[(y + 1) * (m_config.quadCountX + 2) + x + 1]; result -= result / 32.f; m_rippleDest[(y + 1) * (m_config.quadCountX + 2) + x + 1] = result; } } int index = 0; for (int y = 0; y < m_config.quadCountY; ++y) { for (int x = 0; x < m_config.quadCountX; ++x) { // * - denotes current pixel // // a // c * d // b // +1 to both x/y values because the border is padded float a = m_rippleDest[(y)* (m_config.quadCountX + 2) + x + 1]; float b = m_rippleDest[(y + 2) * (m_config.quadCountX + 2) + x + 1]; float c = m_rippleDest[(y + 1) * (m_config.quadCountX + 2) + x]; float d = m_rippleDest[(y + 1) * (m_config.quadCountX + 2) + x + 2]; // NOTE: not so sure about this ... const float offsetFactor = 4096; float s_offset = ((b - a) / offsetFactor); float t_offset = ((c - d) / offsetFactor); // clamp s_offset = (s_offset < -0.5f) ? -0.5f : s_offset; t_offset = (t_offset < -0.5f) ? -0.5f : t_offset; s_offset = (s_offset > 0.5f) ? 0.5f : s_offset; t_offset = (t_offset > 0.5f) ? 0.5f : t_offset; //float s_tc = (float)y / (m_config.quadCountY - 1); //float t_tc = (1.f - (float)x / (m_config.quadCountX - 1)); for (int z = 0; z < 2; ++z) { // first calculate a normalized position into rectangle float s_tc = (float)x / (float)m_config.quadCountX; s_tc *= m_texture->getMaxS(); float t_tc = (float)(y + z) / (float)m_config.quadCountY; t_tc = m_texture->getMaxT() - (t_tc * m_texture->getMaxT()); m_texCoords[index] = ccp(s_tc + s_offset, t_tc + t_offset); ++index; } // NOTE: we calculate extra texture coords here ... // not so sure about this ... if (x == m_config.quadCountX - 1) { for (int z = 0; z < 2; ++z) { float s_tc = 1; s_tc *= m_texture->getMaxS(); float t_tc = (float)(y + z) / (float)m_config.quadCountY; t_tc = m_texture->getMaxT() - (t_tc * m_texture->getMaxT()); m_texCoords[index] = ccp(s_tc + s_offset, t_tc + t_offset); ++index; } } } } // do texture adjust // NOTE: not so sure about this ... for (int y = 1; y < m_config.quadCountY; ++y) { for (int x = 1; x < (m_config.quadCountX + 1) * 2; x += 2) { /* CCPoint preTexCoord = m_texCoords[(y - 1) * (m_config.quadCountX + 1) * 2 + x]; CCPoint curTexCoord = m_texCoords[y * (m_config.quadCountX + 1) * 2 + x - 1]; CCPoint adjustTexCoord = (preTexCoord + curTexCoord) * 0.5f; m_texCoords[(y - 1) * (m_config.quadCountX + 1) * 2 + x] = adjustTexCoord; m_texCoords[y * (m_config.quadCountX + 1) * 2 + x - 1] = adjustTexCoord; */ // NOTE: effect result seems alright ... m_texCoords[(y - 1) * (m_config.quadCountX + 1) * 2 + x] = m_texCoords[y * (m_config.quadCountX + 1) * 2 + x - 1]; } } // swap ripple data buffer std::swap(m_rippleSource, m_rippleDest);}void PhysicsRippleSprite::addRipple(const CCPoint& pos, float strength) { CCSize textureSize = m_texture->getContentSize(); int xIndex = (int)((pos.x / textureSize.width) * m_config.quadCountX); int yIndex = (int)((pos.y / textureSize.height) * m_config.quadCountY); int touchRadius = int(strength * m_config.touchRadius); generateRippleCoeff(touchRadius); for (int y = yIndex - touchRadius; y <= yIndex + touchRadius; ++y) { for (int x = xIndex - touchRadius; x <= xIndex + touchRadius; ++x) { if (x >= 0 && x < m_config.quadCountX && y >= 0 && y < m_config.quadCountY) { // +1 to both x/y values because the border is padded float rippleCoeff = m_rippleCoeffs[touchRadius][(y - (yIndex - touchRadius)) * (touchRadius * 2 + 1) + x - (xIndex - touchRadius)]; m_rippleSource[(y + 1) * (m_config.quadCountX + 2) + x + 1] += rippleCoeff; } } }}void PhysicsRippleSprite::draw(Renderer *renderer, const Mat4& transform, uint32_t flags) { m_customCommand.init(_globalZOrder); m_customCommand.func = CC_CALLBACK_0(PhysicsRippleSprite::onDraw, this, transform); renderer->addCommand(&m_customCommand);}

  还是给张截图~

 

  

 

  # 其他

 

  以上便是目前我所知的实现2D Ripple的方式,如果你还知道其他的方法,那么请务必告知一下 :)

 

3.后记

 

  OK,这次又简单的罗列了一些Ripple Effect2D实现方法,也算是一点点自己的相关总结,有兴致的朋友也可随便参考参考,就这样了,有机会下次再见吧~

你可能感兴趣的文章
为什么基类中的析构函数要声明为虚析构函数?
查看>>
对象切割 - 常量引用传递
查看>>
北邮同学面经
查看>>
Effective C++条款16:成对使用new和delete时要采取相同形式
查看>>
sizeof与strlen
查看>>
一个递归+二分法的洗牌程序
查看>>
YUV格式注释
查看>>
一维、二维数组传参
查看>>
判断当前时间的下一秒是多少
查看>>
从文本文件中读取数据排序并输出到文本
查看>>
求一个整数数组中第二大的数
查看>>
删除一个链表中的节点
查看>>
计算机网络面试整理【转】
查看>>
编译过程的五个阶段
查看>>
Linux系统中的fork()函数详解
查看>>
TCP/IP总结
查看>>
WireShark使用教程
查看>>
UDP总结
查看>>
为什么不能建立引用数组?
查看>>
Union的一个知识点
查看>>