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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏码匠的流水账

聊聊rocketmq的RequestTask

org/apache/rocketmq/remoting/netty/RequestTask.java

1722
来自专栏叁金大数据

EmguCV学习——视频与图片互转

其实视频转图片在上篇文章中已经有些眉目了,其实就是按帧读取视频,然后把帧保存就ok。然后自己再加个进度条美化一下。。。这代码简单易懂,还是直接上代码吧。

1941
来自专栏技术之路

c# 调用win32模拟点击的两种方法

第一种 using System; using System.Collections.Generic; using System.ComponentModel;...

23510
来自专栏菩提树下的杨过

PixelBender(着色器)初体验

只要是玩过photoshop的人,一定会对ps中的各式各样、功能强大的滤镜(filter)留下深刻的印象。 Adobe是靠图形处理软件起家的,这方面一直是它的强...

2796
来自专栏MasiMaro 的技术博文

遍历系统中加载的驱动程序以及通过设备对象指针获取设备对象名称

遍历系统中加载的驱动可以在R3层完成,通过几个未导出的函数:ZwOpenDirectoryObject、ZwQueryDirectoryObject,下面是具体...

1162
来自专栏WOLFRAM

by 落霜枫舞

1523
来自专栏叁金大数据

EmguCV学习——简单算法 差分与高斯

公司项目需要检测运动物体,我对opencv也没啥研究,google了好久看了好多方法,最简单的就是差分与高斯背景建模了。

1483
来自专栏菩提树下的杨过

Flash/Flex学习笔记(40):弹性运动续--弹簧

上一篇里演示的弹性运动加上摩擦力因素后,物体最终基本上都会比较准确的停在目标位置。但是我们回想一下现实世界中的弹簧,如果把弹簧的一头固定起来(即相当于目标点),...

2005
来自专栏跟着阿笨一起玩NET

c#实现打印功能

5612
来自专栏杨建荣的学习笔记

通过java来格式化sql语句(r4笔记第61天)

经常在抓取一些sql语句的时候,得到的sql文本有格式的问题,如果尝试得到执行计划,每次都会费一番周折。 比如下面的sql语句,基本包含了常见的格式问题。第3行...

3444

扫码关注云+社区