Waves1DNode – Water simulation for side-view games.

Forums Programming cocos2d support (graphics engine) Waves1DNode – Water simulation for side-view games.

This topic contains 40 replies, has 19 voices, and was last updated by  quaso 9 months, 1 week ago.

Viewing 25 posts - 1 through 25 (of 41 total)
Author Posts
Author Posts
December 11, 2011 at 3:56 am #237605

slembcke
Administrator
@slembcke

I got the go ahead from Atomic Antelope to share the water simulation code I wrote for Alice in New York.

It uses simple diffusion and vertlet integration. Right now the drawing isn’t very exciting, just an aliased triangle strip, but it could be extended to draw textured water quite easily. I need to make better code to perturb the water too, right now it just changes a single value in the height field which makes jagged looking spikes that eventually smooth out into nice waves.

http://files.slembcke.net/temp/Waves1D.tgz

YouTube video should be up momentarily:

http://www.youtube.com/watch?v=fhjPBvER88g&feature=youtu.be

I have code for doing 2D waves simulation using the same method. Hopefully I’ll have some time tomorrow to finish cleaning up this example and get the 2D waves out as well.

December 11, 2011 at 4:40 am #358627

clarus
Moderator
@clarus

Very cool.

December 11, 2011 at 7:28 am #358628

hiepnd
@hiepnd

Very nice.

Thanks for sharing :)

January 9, 2012 at 3:33 pm #358629

Yann
Participant
@playfripp

Did someone made the same thing but with box2d added or another physic engine ?

Thanks

January 9, 2012 at 3:42 pm #358630

varedis
Moderator
@varedis

I must have missed this when it was first posted. I will defiantly be looking in to this for Fishing Bears’ water

Thanks @slembcke

January 9, 2012 at 3:53 pm #358631

slembcke
Administrator
@slembcke

@Yann. It’s not implemented with a physics engine at all. It’s just a simple heightfield simulation, “1.5D” water if you will. This makes it very very cheap to simulate. Simulating water with a 2D rigid body physics engine is possible, but very expensive to perform.

I never did get around to diffusing how the splash was added to avoid the sharp spikes. Hmm.

January 10, 2012 at 4:39 pm #358632

Yann
Participant
@playfripp

@slembcke

I managed to “group it” with a body to detect collision only. It is the cheapest to code and to run for the device.

I am currently playing with openGL to make more “curvy”, because de sharp spikes are quite annoying :)

Thanks to you indeed !

Yann

January 10, 2012 at 6:05 pm #358633

slembcke
Administrator
@slembcke

Well. What I was doing to make splashes was simply to modify a single column in the heightfield. If you spread that out using a gaussian function or something similar it will be much better looking. Further subdividing the surface before drawing it will make it look much better too (I assume that is what you are talking about?).

January 11, 2012 at 2:38 am #358634

Yann
Participant
@playfripp

@slembcke

In fact it was very easy I just reduced the diffusion to 0.8 and it looks very well. I didn’t had to add more vertices.

I will use it in the final game, can you share the license/legal notice ? (because the game will include all the open source licenses for legal purposes)

Thanks,

Yann

January 11, 2012 at 4:15 am #358635

slembcke
Administrator
@slembcke

Oh, yeah I suppose I should just slap the zlib license on it considering how simple it is.

January 11, 2012 at 10:00 am #358636

Nexus6
Participant
@nexus6

Hi and tnx for this class! I was trying to do the same porting an old actionscript but this class is producing a way better effect with the diffusion set to 0.8.

I have a small OpenGL problem, when i add the water to my play area the part of the background above the water is cleared with black color (or not rendered).

Any advice on how to fix this? Tnx and sorry for my bad english.

Edit:

The problem was caused by another class of my game and was totally unrelated to the Waves1DNode, sorry and tnx again!^^’

January 11, 2012 at 11:21 am #358637

Yann
Participant
@playfripp

@slembcke

Ok, thank you again ;)

January 11, 2012 at 3:39 pm #358638

varedis
Moderator
@varedis

@slembcke

I have implemented this into fishing bears.

Initially I had a problem with the index of the point being used to generate the wave was not anywhere near with where the lure had landed.

This was due to my water bound starting at a value other than 0, I am guessing the method that returns the index will always expect the water bound to start at zero?

Here is a video of what I got after fixing that little hiccup, with a bit of tweaking it could be very nice

January 11, 2012 at 4:21 pm #358639

slembcke
Administrator
@slembcke

Oh, I thought I had added something in there to convert coordinates… Maybe I just didn’t test it very well. Whoops. It was a little rushed because I threw it together on a free Saturday.

January 11, 2012 at 4:21 pm #358640

Duckwit
Participant
@duckwit

Looks snazzy!

@varedis Coming along pretty well, I see :) I was just wondering what that second digit counter is you have onscreen?

January 11, 2012 at 4:49 pm #358641

varedis
Moderator
@varedis

@Duckwit – That is for available memory, the code is available here: http://www.learn-cocos2d.com/2010/07/coding-cocos/

@slembcke – Is that why your code for running the splash called convertTouchToNodeSpace?

January 11, 2012 at 6:18 pm #358642

slembcke
Administrator
@slembcke

@varedis That was the idea yeah. I thought it worked, but I don’t remember how thoroughly I checked.

January 11, 2012 at 7:22 pm #358643

varedis
Moderator
@varedis

@slembcke – If the bounds x value is anything other than 0 then it throws off the index of the vertex that is being picked.

If you change your bounds to equal the below and then try to run it you will notice you get an offset in the wave.

CGRect bounds = CGRectMake(-50.0, 0.0, 480.0, 160.0);

Then the touch will be 5 vertices to the left of where it should have been and the opposite if you change it to a positive 50

I have tried changing the makeSplashAt method to use the _bounds.origin.x as its first parameter like so:

int index = MAX(_bounds.origin.x, MIN((int)(x/[self dx]), _count - 1));

But that made no difference, I am not entirely sure what that line is doing, would you mind explaining?

January 11, 2012 at 7:59 pm #358644

slembcke
Administrator
@slembcke

Ah, so In HelloWorldLayer.m I convert the coordinates like this:

[_waves makeSplashAt:[_waves convertTouchToNodeSpace:[touches anyObject]].x];

That was what was converting the coordinates. It should probably get moved into the Wave1DNode instead.

The clamping just prevents it from modifying the array outside of it’s bounds.

April 25, 2012 at 12:37 pm #358645

cocosroot
Participant
@cocosroot

Would it be possible for anyone knowledgeable enough to convert this to OpenGL ES 2.0? Would be greatly appreciated :)

April 25, 2012 at 9:29 pm #358646

Nexus6
Participant
@nexus6
#import "cocos2d.h"

@interface Waves1DNode : CCNode {
CGRect _bounds;

float _diffusion;
float _damping;

int _count;
// Heightfields that the simulation vertlet integrates between.
float *_h1, *_h2;

int textureSize;
CCSprite *_water;
GLuint _textureLocation;

float offset;

}
@property (nonatomic) float speed;
@property (nonatomic, retain) CCSprite *water;

// 'bounds' are the rectangle to draw for the water. The top of the bounds is the rest height for the water, it wil wave above and below it.
// 'count' is the number of slices to simulate. One per 10-20 pixels is usually sufficient.
// 'damping' is how fast the water settles back to rest. 1.0 is never (bad), 0.0 is immediately (also bad). 0.99 is a decent damping amount.
// 'diffusion' is how fast the waves spread to neighbors. Values outside of 0.6 - 0.9 can become unstable.
-(id)initWithBounds:(CGRect)bounds count:(int)count damping:(float)damping diffusion:(float)diffusion;

-(void)makeSplashAt:(float)x;

@end

#import "Waves1DNode.h"

static CCGLProgram *shader_ = nil;

@implementation Waves1DNode

@synthesize speed = _speed;
@synthesize water = _water;

-(id)initWithBounds:(CGRect)bounds count:(int)count damping:(float)damping diffusion:(float)diffusion;
{
if((self = [super init])){
_bounds = bounds;
_count = count;
_damping = damping;
_diffusion = diffusion;

_h1 = calloc(_count, sizeof(float));
_h2 = calloc(_count, sizeof(float));

_speed = 0;

offset = 0;

shader_ = [[CCGLProgram alloc] initWithVertexShaderFilename:@"PositionTexture.vsh"
fragmentShaderFilename:@"PositionTexture.fsh"];
[shader_ addAttribute:kCCAttributeNamePosition index:kCCVertexAttrib_Position];
[shader_ addAttribute:kCCAttributeNameTexCoord index:kCCVertexAttrib_TexCoords];
[shader_ link];
[shader_ updateUniforms];

_textureLocation = glGetUniformLocation( shader_->program_, "u_texture");

#ifndef DRAW_BOX2D_WORLD
textureSize = 256*CC_CONTENT_SCALE_FACTOR();

self.water = [CCSprite spriteWithFile:@"Water.png"];

ccTexParams tp = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_CLAMP_TO_EDGE};
[self.water.texture setTexParameters:&tp];

#endif
}

return self;
}

- (void) dealloc
{
free(_h1);
free(_h2);

#ifndef DRAW_BOX2D_WORLD
self.water = nil;
#endif

[super dealloc];
}

-(void)vertlet {
offset -= 0.1f;

for(int i=0; i<_count; i++) _h1 = 2.0*_h2 - _h1;

float *temp = _h2;
_h2 = _h1;
_h1 = temp;
}

static inline float
diffuse(float diff, float damp, float prev, float curr, float next){
return (curr*diff + ((prev + next)*0.5f)*(1.0f - diff))*damp;
}

-(void)diffuse {
float prev = _h2[0];
float curr = _h2[0];
float next = _h2[1];

_h2[0] = diffuse(_diffusion, _damping, prev, curr, next);

for(int i=1; i<(_count - 1); ++i){
prev = curr;
curr = next;
next = _h2
;

_h2
= diffuse(_diffusion, _damping, prev, curr, next);
}

prev = curr;
curr = next;
_h2[_count - 1] = diffuse(_diffusion, _damping, prev, curr, next);
}

-(float)dx{return _bounds.size.width/(GLfloat)(_count - 1);}

- (void)draw {

// It would be better to run these on a fixed timestep.
// As an GFX only effect it doesn't really matter though.
[self vertlet];
[self diffuse];

GLfloat dx = [self dx];
GLfloat top = _bounds.size.height;

// Build a vertex array and render it.
struct Vertex{GLfloat x,y;};
struct Vertex verts[_count*2];
struct TexVertex{GLfloat x,y;};
struct TexVertex texVerts[_count*2];
for(int i=0; i<_count; i++){
GLfloat x = i*dx;
verts[2*i + 0] = (struct Vertex){x, (top + _h2
)};
verts[2*i + 1] = (struct Vertex){x, (top + _h2
)-(float)textureSize};
texVerts[2*i + 0] = (struct TexVertex){(x-offset)/(float)textureSize, 0};
texVerts[2*i + 1] = (struct TexVertex){(x-offset)/(float)textureSize, 1.0f};
}

ccGLBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );

ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords );
ccGLUseProgram( shader_->program_ );
ccGLUniformModelViewProjectionMatrix( shader_ );

ccGLBindTexture2D([self.water.texture name]);

glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, verts);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, texVerts);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei) _count*2);

}

-(void)makeSplashAt:(float)x;
{
// Changing the values of heightfield in h2 will make the waves move.
// Here I only change one column, but you get the idea.
// Change a bunch of the heights using a nice smoothing function for a better effect.

int index = MAX(0, MIN((int)(x/[self dx]), _count - 1));
_h2[index] += _speed;

}

@end

I’m not sure that this code is good but i’m using it and it seems to work. This version uses a texture.

April 25, 2012 at 9:40 pm #358647

cocosroot
Participant
@cocosroot

Thank you, this is great! :)

April 25, 2012 at 9:45 pm #358648

Nexus6
Participant
@nexus6

Glad to help! Just noticed that I forgot to clean the code… please beware about those useless box2d #defines and the useless _speed property, it just make the texture scroll because it was a needing of my prototype.

June 15, 2012 at 7:37 am #358649

xMonty
Participant
@xmonty

i am new to cocos2d, so can someone please tell me what should be in (PositionTexture.fsh, PositionTexture.vsh and what does water.png look like)

August 8, 2012 at 12:09 pm #358650

devindazzle
Participant
@devindazzle

I have tried to update the code to Cocos2d version 2.0 using a shader with position/color (no texture). The code is pasted below … It will not render and I am very very sure it is because I am still not getting OpenGL ES 2.0 in Cocos2d. Can anyone help me to understand why it does not work?

==========================================================================

// Water.h

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface Water : CCNode
{
CGRect _bounds;
int _count;
float _diffusion;
float _damping;
float _offset;

float *_h1;
float *_h2;
}

// 'bounds' are the rectangle to draw for the water. The top of the 'bounds' is the rest height for the water, it will wave above and below it
// 'count' is the number of slices to simulate. One per 10-20 px is usually sufficient
// 'damping' is how fast the water settels back to rest. 1.0 is never (bad), 0.0 is immidiately (also bad). 0.99 is a decent dampening amount
// 'diffusion' is how fast the waves distribute to neighbours. Values outside of 0.6 - 0.9 can become unstable
+ (id) waterWithBounds:(CGRect)bounds count:(int)count damping:(float)damping diffusion:(float)diffusion;

// Will create a splash at the given x location within the water bounds
- (void) makeSplashAt:(float)x;

@end

==========================================================================

// Water.m
#import "Water.h"

static CCGLProgram *shader_ = nil;
static int colorLocation_ = -1;
static int pointSizeLocation_ = -1;
static ccColor4F color_ = { 0.0f, 0.0f, 1.0f, 0.3f };

@implementation Water

+ (id) waterWithBounds:(CGRect)bounds count:(int)count damping:(float)damping diffusion:(float)diffusion
{
return [[self alloc] initWithBounds:bounds count:count damping:damping diffusion:diffusion];
}

- (id) initWithBounds:(CGRect)bounds count:(int)count damping:(float)damping diffusion:(float)diffusion
{

if ((self = [super init]))
{

// Store variables
_bounds = bounds;
_count = count;
_damping = damping;
_diffusion = diffusion;

_h1 = calloc(_count, sizeof(float));
_h2 = calloc(_count, sizeof(float));

// Set content size
// self.contentSize = CGSizeMake(bounds.size.width - bounds.origin.x, bounds.size.height - bounds.origin.y);

// Attach shaders - use build in shaders for now
self.shaderProgram = [[CCGLProgram alloc] initWithVertexShaderByteArray:ccPosition_uColor_vert fragmentShaderByteArray:ccPosition_uColor_frag];

[self.shaderProgram addAttribute:kCCAttributeNamePosition index:kCCVertexAttrib_Position];
[self.shaderProgram addAttribute:kCCAttributeNameColor index:kCCVertexAttrib_Color];
[self.shaderProgram link];
[self.shaderProgram updateUniforms];

/* shader_ = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_Position_uColor];

colorLocation_ = glGetUniformLocation(shader_->program_, "u_color");
pointSizeLocation_ = glGetUniformLocation(shader_->program_, "u_pointSize"); */

CCLOG(@"Water created at %@", NSStringFromCGRect(bounds));

}
return self;

}

- (void) dealloc
{
free(_h1);
free(_h2);
}

- (void) makeSplashAt:(float)x
{

int index = MAX(0, MIN((int)(x / [self dx]), _count - 1));

_h2[index] += CCRANDOM_MINUS1_1() * 20.0f;

}

- (void) draw
{

[self verlet];
[self diffuse];

GLfloat dx = [self dx];
GLfloat top = _bounds.size.height;

// Build a vertex array and render it
struct Vertex { GLfloat x, y; };
struct Vertex verts[_count * 2];

for (int i = 0; i < _count; i++)
{
GLfloat x = i * dx;

verts[2 * i + 0] = (struct Vertex) { x, 0 };
verts[2 * i + 1] = (struct Vertex) { x, (top + _h2) };
}

const GLfloat triColor[] =
{
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
};

ccGLBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );
ccGLUseProgram(shaderProgram_ ->program_);

CHECK_GL_ERROR_DEBUG();

ccGLEnableVertexAttribs(kCCVertexAttrib_Position | kCCVertexAttrib_Color);

CHECK_GL_ERROR_DEBUG();

glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, verts);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_FLOAT, GL_FALSE, 0, triColor);

CHECK_GL_ERROR_DEBUG();

glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei) _count * 2);

CHECK_GL_ERROR_DEBUG();

/* [shader_ use];
[shader_ setUniformForModelViewProjectionMatrix];
[shader_ setUniformLocation:colorLocation_ with4fv:(GLfloat *) &color_.r count:1];

ccGLEnableVertexAttribs( kCCVertexAttrib_Position );

glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, verts);

glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei) _count * 2);

CC_INCREMENT_GL_DRAWS(1); */
}

- (void) verlet
{
for (int i = 0; i < _count; i++)
{
_h1
= 2.0 * _h2 - _h1;
}

float *temp = _h2;
_h2 = _h1;
_h1 = temp;
}

static inline float diffuse(float diff, float damp, float prev, float curr, float next)
{
return (curr * diff + ((prev + next) * 0.5f) * (1.0f - diff)) * damp;
}

- (void) diffuse
{
float prev = _h2[0];
float curr = _h2[0];
float next = _h2[0];

_h2[0] = diffuse(_diffusion, _damping, prev, curr, next);

for (int i = 1; i < (_count - 1); ++i)
{
prev = curr;
curr = next;
next = _h2
;

_h2
= diffuse(_diffusion, _damping, prev, curr, next);
}

prev = curr;
curr = next;
_h2[_count - 1] = diffuse(_diffusion, _damping, prev, curr, next);
}

- (float)dx
{
return _bounds.size.width / (GLfloat)(_count - 1);
}

@end

Viewing 25 posts - 1 through 25 (of 41 total)

You must be logged in to reply to this topic.