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
#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, m_rippleList, RippleList)
protected:
// render command
CustomCommand m_customCommand;
};
#endif
// pgeRippleSprite.cpp
#include "pgeRippleSprite.h"
pgeRippleSprite* pgeRippleSprite::create(const char* filename)
{
auto sprite = new (std::nothrow) pgeRippleSprite();
if (sprite && sprite->initWithFile(filename))
{
sprite->autorelease();
return sprite;
}
CC_SAFE_DELETE(sprite);
return NULL;
}
pgeRippleSprite* pgeRippleSprite::create(CCTexture2D* texture)
{
auto sprite = new (std::nothrow) pgeRippleSprite();
if (sprite && sprite->initWithTexture(texture))
{
sprite->autorelease();
return sprite;
}
CC_SAFE_DELETE(sprite);
return NULL;
}
pgeRippleSprite::pgeRippleSprite()
:m_texture(NULL),
m_vertice(NULL),
m_textureCoordinate(NULL),
m_rippleCoordinate(NULL),
m_edgeVertice(NULL)
{
}
pgeRippleSprite::~pgeRippleSprite()
{
CC_SAFE_RELEASE(m_texture);
CC_SAFE_DELETE_ARRAY(m_vertice);
CC_SAFE_DELETE_ARRAY(m_textureCoordinate);
CC_SAFE_DELETE_ARRAY(m_rippleCoordinate);
CC_SAFE_DELETE_ARRAY(m_edgeVertice);
clearRipples();
}
bool pgeRippleSprite::initWithFile(const char* filename)
{
return initWithTexture(CCTextureCache::sharedTextureCache()->addImage(filename));
}
bool pgeRippleSprite::initWithTexture(CCTexture2D* texture)
{
m_texture = texture;
if (!m_texture) return false;
m_texture->retain();
m_vertice = NULL;
m_textureCoordinate = NULL;
CC_SAFE_DELETE_ARRAY(m_vertice);
CC_SAFE_DELETE_ARRAY(m_textureCoordinate);
CC_SAFE_DELETE_ARRAY(m_rippleCoordinate);
CC_SAFE_DELETE_ARRAY(m_edgeVertice);
m_quadCountX = RIPPLE_DEFAULT_QUAD_COUNT_X;
m_quadCountY = RIPPLE_DEFAULT_QUAD_COUNT_Y;
m_inverse = false;
tesselate();
scheduleUpdate();
setContentSize(m_texture->getContentSize());
//setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture));
setGLProgram(ShaderCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE));
return true;
}
void pgeRippleSprite::onDraw(const Mat4& transform, uint32_t flags)
{
getGLProgram()->use();
getGLProgram()->setUniformsForBuiltins(transform);
GL::bindTexture2D(m_texture->getName());
GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POSITION | GL::VERTEX_ATTRIB_FLAG_TEX_COORD);
// TODO: use VBO or even VAO
glBindBuffer(GL_ARRAY_BUFFER, 0);
float* vertexBuffer = NULL;
float* coordBuffer = NULL;
CCPoint* coordSource = (m_rippleList.size() == 0) ? m_textureCoordinate : m_rippleCoordinate;
if (sizeof(CCPoint) == sizeof(ccVertex2F))
{
vertexBuffer = (float*)m_vertice;
coordBuffer = (float*)coordSource;
}
else
{
// NOTE: clear these soon
static float* s_vertexBuffer = new float[2 * m_VerticesPrStrip * m_quadCountY];
static float* s_coordBuffer = new float[2 * m_VerticesPrStrip * m_quadCountY];
for (int i = 0; i < m_VerticesPrStrip * m_quadCountY; ++i)
{
s_vertexBuffer[i * 2] = m_vertice[i].x;
s_vertexBuffer[i * 2 + 1] = m_vertice[i].y;
s_coordBuffer[i * 2] = coordSource[i].x;
s_coordBuffer[i * 2 + 1] = coordSource[i].y;
}
vertexBuffer = s_vertexBuffer;
coordBuffer = s_coordBuffer;
}
glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertexBuffer);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, coordBuffer);
for (int strip = 0; strip < m_quadCountY; ++strip)
{
glDrawArrays(GL_TRIANGLE_STRIP, strip * m_VerticesPrStrip, m_VerticesPrStrip);
}
}
void pgeRippleSprite::clearRipples()
{
auto iterBegin = m_rippleList.begin();
while (iterBegin != m_rippleList.end())
{
RippleData* date = *iterBegin;
CC_SAFE_DELETE(date);
iterBegin++;
}
m_rippleList.clear();
}
void pgeRippleSprite::tesselate()
{
CC_SAFE_DELETE_ARRAY(m_vertice);
CC_SAFE_DELETE_ARRAY(m_textureCoordinate);
CC_SAFE_DELETE_ARRAY(m_rippleCoordinate);
CC_SAFE_DELETE_ARRAY(m_edgeVertice);
m_VerticesPrStrip = 2 * (m_quadCountX + 1);
m_bufferSize = m_VerticesPrStrip * m_quadCountY;
//allocate buffers
m_vertice = new CCPoint[m_bufferSize];
m_textureCoordinate = new CCPoint[m_bufferSize];
m_rippleCoordinate = new CCPoint[m_bufferSize];
m_edgeVertice = new bool[m_bufferSize];
int vertexPos = 0;
CCPoint normalized;
CCSize contentSize = m_texture->getContentSize();
for (int y = 0; y < m_quadCountY; ++y)
{
for (int x = 0; x < (m_quadCountX + 1); ++x)
{
for (int yy = 0; yy < 2; ++yy)
{
// first simply calculate a normalized position into rectangle
normalized.x = (float)x / (float)m_quadCountX;
normalized.y = (float)(y + yy) / (float)m_quadCountY;
// calculate vertex by multiplying rectangle ( texture ) size
m_vertice[vertexPos] = ccp(normalized.x * contentSize.width, normalized.y * contentSize.height);
// adjust texture coordinates according to texture size
// as a texture is always in the power of 2, maxS and maxT are the fragment of the size actually used
// invert y on texture coordinates
m_textureCoordinate[vertexPos] = ccp(normalized.x * m_texture->getMaxS(), m_texture->getMaxT() - (normalized.y * m_texture->getMaxT()));
// check if vertice is an edge vertice, because edge vertices are never modified to keep outline consistent
m_edgeVertice[vertexPos] = (
(x == 0) ||
(x == m_quadCountX) ||
((y == 0) && (yy == 0)) ||
((y == (m_quadCountY - 1)) && (yy > 0)));
// next buffer pos
++vertexPos;
}
}
}
}
void pgeRippleSprite::addRipple(const cocos2d::CCPoint &pos, RippleType type, float strength)
{
// allocate new ripple
RippleData* newRipple = new RippleData();
// initialize ripple
newRipple->parent = true;
for (int count = 0; count < 4; ++count)
{
newRipple->childCreated[count] = false;
}
newRipple->rippleType = type;
newRipple->center = pos;
CCSize contentSize = m_texture->getContentSize();
newRipple->centerCoordinate = ccp(pos.x / contentSize.width * m_texture->getMaxS(), m_texture->getMaxT() - (pos.y / contentSize.height * m_texture->getMaxT()));
newRipple->radius = RIPPLE_DEFAULT_RADIUS;
newRipple->strength = strength;
newRipple->runtime = 0;
newRipple->currentRadius = 0;
newRipple->rippleCycle = RIPPLE_DEFAULT_RIPPLE_CYCLE;
newRipple->lifespan = RIPPLE_DEFAULT_LIFESPAN;
// add ripple to running list
m_rippleList.push_back(newRipple);
}
void pgeRippleSprite::addRippleChild(RippleData* parent, RippleChildType type)
{
// allocate new ripple
RippleData* newRipple = new RippleData();
CCPoint pos;
// new ripple is pretty much a copy of its parent
memcpy(newRipple, parent, sizeof(RippleData));
// not a parent
newRipple->parent = false;
CCSize winSize = CCDirector::sharedDirector()->getWinSize();
// mirror position
switch (type) {
case RippleChildType::Left:
pos = ccp(-parent->center.x, parent->center.y);
break;
case RippleChildType::Top:
pos = ccp(parent->center.x, winSize.height + (winSize.height - parent->center.y));
break;
case RippleChildType::Right:
pos = ccp(winSize.width + (winSize.width - parent->center.x), parent->center.y);
break;
case RippleChildType::Bottom:
default:
pos = ccp(parent->center.x, -parent->center.y);
break;
}
newRipple->center = pos;
CCSize contentSize = m_texture->getContentSize();
newRipple->centerCoordinate = ccp(pos.x / contentSize.width * m_texture->getMaxS(), m_texture->getMaxT() - (pos.y / contentSize.height * m_texture->getMaxT()));
newRipple->strength *= RIPPLE_CHILD_MODIFIER;
// indicate child used
parent->childCreated[(unsigned)type] = true;
// add ripple to running list
m_rippleList.push_back(newRipple);
}
void pgeRippleSprite::update(float dt)
{
// test if any ripples at all
if (m_rippleList.size() == 0) return;
RippleData* ripple;
CCPoint pos;
float distance, correction;
// ripples are simulated by altering texture coordinates
// on all updates, an entire new array is calculated from the base array
// not maintaining an original set of texture coordinates, could result in accumulated errors
memcpy(m_rippleCoordinate, m_textureCoordinate, m_bufferSize * sizeof(CCPoint));
// scan through running ripples
// the scan is backwards, so that ripples can be removed on the fly
CCSize winSize = CCDirector::sharedDirector()->getWinSize();
auto iterRipple = m_rippleList.rbegin();
while (iterRipple != m_rippleList.rend())
{
// get ripple data
ripple = *iterRipple;
// scan through all texture coordinates
for (int count = 0; count < m_bufferSize; ++count)
{
// don't modify edge vertices
if (!m_edgeVertice[count])
{
// calculate distance
// you might think it would be faster to do a box check first
// but it really isn't,
// ccpDistance is like my sexlife - BAM! - and its all over
distance = ccpDistance(ripple->center, m_vertice[count]);
// only modify vertices within range
if (distance <= ripple->currentRadius)
{
// load the texture coordinate into an easy to use var
pos = m_rippleCoordinate[count];
// calculate a ripple
switch (ripple->rippleType)
{
case RippleType::Rubber:
// method A
// calculate a sinus, based only on time
// this will make the ripples look like poking a soft rubber sheet, since sinus position is fixed
correction = sinf(2 * M_PI * ripple->runtime / ripple->rippleCycle);
break;
case RippleType::Gel:
// method B
// calculate a sinus, based both on time and distance
// this will look more like a high viscosity fluid, since sinus will travel with radius
correction = sinf(2 * M_PI * (ripple->currentRadius - distance) / ripple->radius * ripple->lifespan / ripple->rippleCycle);
break;
case RippleType::Water:
default:
// method c
// like method b, but faded for time and distance to center
// this will look more like a low viscosity fluid, like water
correction = (ripple->radius * ripple->rippleCycle / ripple->lifespan) / (ripple->currentRadius - distance);
if (correction > 1.0f) correction = 1.0f;
// fade center of quicker
correction *= correction;
correction *= sinf(2 * M_PI * (ripple->currentRadius - distance) / ripple->radius * ripple->lifespan / ripple->rippleCycle);
break;
}
// fade with distance
correction *= 1 - (distance / ripple->currentRadius);
// fade with time
correction *= 1 - (ripple->runtime / ripple->lifespan);
// adjust for base gain and user strength
correction *= RIPPLE_BASE_GAIN;
correction *= ripple->strength;
// finally modify the coordinate by interpolating
// because of interpolation, adjustment for distance is needed,
correction /= ccpDistance(ripple->centerCoordinate, pos);
pos = ccpAdd(pos, ccpMult(ccpSub(pos, ripple->centerCoordinate), correction));
// another approach for applying correction, would be to calculate slope from center to pos
// and then adjust based on this
// clamp texture coordinates to avoid artifacts
pos = ccpClamp(pos, Vec2::ZERO, ccp(m_texture->getMaxS(), m_texture->getMaxT()));
// save modified coordinate
m_rippleCoordinate[count] = pos;
}
}
}
// calculate radius
ripple->currentRadius = ripple->radius * ripple->runtime / ripple->lifespan;
// check if ripple should expire
ripple->runtime += dt;
if (ripple->runtime >= ripple->lifespan)
{
// free memory, and remove from list
CC_SAFE_DELETE(ripple);
auto it = --iterRipple.base();
auto it_after_del = m_rippleList.erase(it);
iterRipple = std::list::reverse_iterator(it_after_del);
}
else
{
// check for creation of child ripples
// NOTE: now we do not need this
/*
if (ripple->parent == true)
{
// left ripple
if ((ripple->childCreated[(unsigned)RippleChildType::Left] == false) && (ripple->currentRadius > ripple->center.x))
{
addRippleChild(ripple, RippleChildType::Left);
}
// top ripple
if ((ripple->childCreated[(unsigned)RippleChildType::Top] == false) && (ripple->currentRadius > winSize.height - ripple->center.y))
{
addRippleChild(ripple, RippleChildType::Top);
}
// right ripple
if ((ripple->childCreated[(unsigned)RippleChildType::Right] == false) && (ripple->currentRadius > winSize.width - ripple->center.x))
{
addRippleChild(ripple, RippleChildType::Right);
}
// bottom ripple
if ((ripple->childCreated[(unsigned)RippleChildType::Bottom] == false) && (ripple->currentRadius > ripple->center.y))
{
addRippleChild(ripple, RippleChildType::Bottom);
}
}
*/
iterRipple++;
}
}
}
void pgeRippleSprite::setInverse(bool inverse)
{
if (inverse != m_inverse)
{
m_inverse = inverse;
for (int i = 0; i < m_VerticesPrStrip * m_quadCountY; ++i)
{
m_textureCoordinate[i].y = 1.0f - m_textureCoordinate[i].y;
}
}
}
void pgeRippleSprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) {
m_customCommand.init(_globalZOrder);
m_customCommand.func = CC_CALLBACK_0(pgeRippleSprite::onDraw, this, transform, flags);
renderer->addCommand(&m_customCommand);
}
仍旧给张截图~
# 物理模拟
目前个人感觉效果最好的波纹实现方式,当然,这里所谓的物理只是简单的模拟了水波传递和消减的过程,与什么流体动力学没有多大关系,但即便如此,效果感觉也是非常真实的,毕竟其实现方式遵循了一定的物理原则,而我们人类感知的基础其实也就是这种种物理法则罢了,另外,这种实现方式还有一个极大的好处,就是其不存在波纹数量的限制,而上面提到的两种方式都没有这个优点,一旦波纹数量增多,效率的损失就非常明显~
相关的原理说明,网上已有了非常好的教程(这里,这里也有一个挺有意思的相关解说),以下列出的代码其实大部分参照了苹果的一个Sample(这里),有兴趣的朋友可以仔细看看:
// PhysicsRippleSprite.h
#ifndef __PHYSICS_RIPPLE_SPRITE_H__
#define __PHYSICS_RIPPLE_SPRITE_H__
#include
// PhysicsRippleSprite.cpp
#include "PhysicsRippleSprite.h"
#include
PhysicsRippleSprite*
PhysicsRippleSprite::create(const char* filename, const PhysicsRippleSpriteConfig& config) {
auto rippleSprite = new PhysicsRippleSprite();
if (rippleSprite && rippleSprite->init(filename, config)) {
rippleSprite->autorelease();
return rippleSprite;
}
else {
CC_SAFE_DELETE(rippleSprite);
return nullptr;
}
}
PhysicsRippleSprite*
PhysicsRippleSprite::create(CCTexture2D* texture, const PhysicsRippleSpriteConfig& config) {
auto rippleSprite = new PhysicsRippleSprite();
if (rippleSprite && rippleSprite->init(texture, config)) {
rippleSprite->autorelease();
return rippleSprite;
}
else {
CC_SAFE_DELETE(rippleSprite);
return nullptr;
}
}
PhysicsRippleSprite::~PhysicsRippleSprite() {
CC_SAFE_RELEASE(m_texture);
CC_SAFE_DELETE_ARRAY(m_vertices);
CC_SAFE_DELETE_ARRAY(m_texCoords);
for (auto kv : m_rippleCoeffs) {
CC_SAFE_DELETE_ARRAY(kv.second);
}
CC_SAFE_DELETE_ARRAY(m_rippleSource);
CC_SAFE_DELETE_ARRAY(m_rippleDest);
}
bool PhysicsRippleSprite::init(const char* filename, const PhysicsRippleSpriteConfig& config) {
auto texture = CCTextureCache::sharedTextureCache()->addImage(filename);
return init(texture, config);
}
bool PhysicsRippleSprite::init(CCTexture2D* texture, const PhysicsRippleSpriteConfig& config) {
if (!texture) {
return false;
}
m_texture = texture;
m_texture->retain();
m_config = config;
initRippleBuffer();
initRippleCoeff();
initRippleMesh();
setContentSize(m_texture->getContentSize());
setGLProgram(ShaderCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE));
//setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture));
scheduleUpdate();
return true;
}
void PhysicsRippleSprite::reset() {
// now we just reset ripple height data
memset(m_rippleSource, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float));
memset(m_rippleDest, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float));
// reset elapse time
m_elapseTime = 0;
}
void PhysicsRippleSprite::initRippleBuffer() {
m_rippleSource = new float[(m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)];
m_rippleDest = new float[(m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float)];
// +2 for padding the border
memset(m_rippleSource, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float));
memset(m_rippleDest, 0, (m_config.quadCountX + 2) * (m_config.quadCountY + 2) * sizeof(float));
}
void PhysicsRippleSprite::initRippleCoeff() {
generateRippleCoeff(m_config.touchRadius);
}
// TODO: 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实现方法,也算是一点点自己的相关总结,有兴致的朋友也可随便参考参考,就这样了,有机会下次再见吧~