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