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

时间: 2024-10-10 10:05:29

随便聊聊水面效果的2D实现(二)的相关文章

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

0. 引子 一直想随便写写自己关于水面效果2D实现的一些了解,可惜各种原因一直拖沓,幸而近来有些事情终算告一段落,自己也有了一些闲暇时间,于是便有了这篇东西 :) 1. 概述 关于水面效果的实现方法,google一下非常之多,目前的很多游戏也有非常好的呈现,其中最令我印象深刻的当数<Crysis>~ 自己由于工作原因接触过一段时间的CryEngine,对于Crysis的水面渲染有一点点的了解,当然其中细节非常复杂,但就基本原理来讲,就是将整块水面细分成适当粒度的三角面,然后通过动态改变各个三角

看了 陈记抄 同学 的 《随便聊聊之量子力学中的散射理论》

看了 陈记抄 同学 的 <随便聊聊之量子力学中的散射理论>  http://tieba.baidu.com/p/6375956937     , 我肯定了一件事,     空间微积分 把   理论物理学  带上 歧路 了    . 我可以 郑重 的 跟 大家 宣布,    理论物理学 的  基本 理论 和 公式 不需要 空间微积分,   应使用 线性 离散 样本 + 二维特例 + 二维古典微积分 来 解决问题   . 参考 我 在 反相吧  发的 帖 <对于 麦克斯韦方程, 说一次 我要

leetcode——Search a 2D Matrix 二维有序数组查找(AC)

Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the following properties: Integers in each row are sorted from left to right. The first integer of each row is greater than the last integer of the previous ro

[LeetCode] Range Sum Query 2D - Mutable 二维区域和检索 - 可变

Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2). The above rectangle (with the red border) is defined by (row1, col1) = (2, 1) and (row2, co

随便说说堆(一)——二叉堆

二叉堆可以被看作是一个数组,也可以简单的看作是一个近似的完全二叉树,二叉堆有最大堆和最小堆,分别具有堆的性质:最大堆的某个结点的值最多与其父结点一样大,最小堆则是某个结点的值最多与其父结点一样小.所以最大堆中最大的结点永远是根结点,最小堆中最小的结点永远是根节点. 既然二叉堆是一种数据结构,就有其支持的操作(这里以最小堆为例): make_heap:建立一个空堆,或者把数组中元素转换成二叉堆. insert:插入元素. minimun:返回一个最小数. extract_min:移除最小结点. u

聊聊高并发(十二)分析java.util.concurrent.atomic.AtomicStampedReference源码来看如何解决CAS的ABA问题

在聊聊高并发(十一)实现几种自旋锁(五)中使用了java.util.concurrent.atomic.AtomicStampedReference原子变量指向工作队列的队尾,为何使用AtomicStampedReference原子变量而不是使用AtomicReference是因为这个实现中等待队列的同一个节点具备不同的状态,而同一个节点会多次进出工作队列,这就有可能出现出现ABA问题. 熟悉并发编程的同学应该知道CAS操作存在ABA问题.我们先看下CAS操作. CAS(Compare and

随便玩玩Django--输入网址生成二维码

在自强学堂上学习了下django,自己花了点时间写个输入网址生成二维码的网页.大概思路:在前端网页输入要转化成二维码的网址,网页提交表单通过urls.py找到views.py相应的方法,生成二维码图片.动手玩玩. 创建项目 django-admin.py startproject lsk_tool 在新建的项目里新建一个app python manage.py startapp tools 在app中新建templates文件夹,把写好的网页文件夹中,index.html代码如下: <!DOCT

[leetcode]304. Range Sum Query 2D - Immutable二维区间求和 - 不变

Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2). The above rectangle (with the red border) is defined by (row1, col1) = (2, 1) and (row2, co

编程道拓扑bcd.top 0x01/ 开局第一篇: 随便聊聊/ 随笔

0x01 开局 编程道拓扑(bcd.top)是一个前端从业者的思考和总结, 如果你喜欢, 欢迎关注! 作者是一个前端从业者, 本系列会总结作者在工作和学习中的一些思考, 会有具体的技术点, 也会有关于编程的一些鸡汤思考. 开局第一篇, 先来点思考! 编程道核心是什么 笔者观点: 复用世界, 但是不要复制自己 我现在的观点是, 编程就是复用, 复用别人的工作, 复用别人的经验,当然, 请不要简单的理解成 粘贴复制, 粘贴复制在笔者或者大部分的从业者看来应该都是没有什么技术含量的, 笔者这里的观点是