随便聊聊水面效果的2D实现(二)

时间:2022-06-05
本文章向大家介绍随便聊聊水面效果的2D实现(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

0. 引子

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

1. 概述

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

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

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

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

2. 方法

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

  # 使用Shader

  如果看过上篇的朋友一定了解,在实现2D的Water效果时,我多次使用了Fragment Shader,而对于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_rippleDistance及u_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 <list>

#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


// --------------------------------------------------------------------------
// typedefs


enum 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       
};


// --------------------------------------------------------------------------
// pgeRippleSprite

class 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 flag
protected:
	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<RippleData*>, 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<RippleData*>::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 <map>
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<int, float*> 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 <algorithm>

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: improve
void 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: improve
void 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 Effect的2D实现方法,也算是一点点自己的相关总结,有兴致的朋友也可随便参考参考,就这样了,有机会下次再见吧~