Writing a texture plugin to produce water effects

Texture plugin to produce water effects

Hello - I am foolishly toying with the idea of converting Scott Scriven’s water algorithm (see the SDL site here: http://www.libsdl.org/projects/water/) into some kind of texture plugin for Blender.

It’s a great effect, based on an old fluke. It creates very convincing water-like effects, the waves even reflect back off the sides of the image!

I am really just starting to think about this, I don’t really know C let alone understand textures and normals and vectors and … sheesh.

I come from the “old school” of Basic-A and Z80-A
assembly, but I am a designer not a programmer.

Enough excuses! I thought I would list what I have in mind, and then ask a few questions.

Ideas:

  1. Load an image for a backdrop to the ripples.
  2. Use the ripples as bumps.
  3. Choose from various effects like:
    • Rain drops
    • “Trails” As if a stick is drawn through the water.
    • A single drop at x,y
    • Other settings as in the SDL code, jelly, sludge, speed etc.
  4. Set the ‘frame’ number of the effect: since the waves must propogate to create the effect, you need to run the function X number of times to reach a certain look.
  5. Have it animate. I picture mapping this texture to a plane and then letting it run until the waves die away.
  6. Maybe other settings like start/stop/fade-in/fade-out etc.

Questions:
From what I can see (and I have toyed with this code in python and, in the past, in VB6 [I’m on Linux now!] ), you need to know the width and the height of the rectangular area you are working in.

  1. Is there some way for a width and height to reach the “plugin_tex_doit” function in the plugin? I saw the dxt and dyt vars, but they require OSA to be on in render settings.
  2. Is there a way to make this algorithm work without knowing the limits of the area?
  3. I thought that it would require a kind-of pre-calculation whereby the entire area is placed into an array and that array is then stepped through by the “plugin_tex_doit” function. When a frame (in Blender) passes to a new one, the array is re-built based on the old one and so on.
  4. Any idea how to choose and load a bitmap into the plugin?

Well, perhaps I have chosen an impossible itch to scratch? Perhaps it would be easier to write an app to output a video of the water effect and then to use that as a texture!
Any word?
:slight_smile:

Hmmm. interesting, i’ll take a look at the site when i get home, hope this works out for you.

MacBlender

Great!
Any tips at all about texture plugins and the C related to them would be grand.

I wish we could do textures based on Python - the speed would be an issue, but the ease might mitigate that.

Cheers.

you might email the bf-python list and ask about the possibility of exposing the texture stuff to python.

LetterRip

K i looked at the site, the only thing i can see being a large problem would be the fact that its designed around that SDL. which means you’ll either have to repackage SDL with it or rework the entire thing with the algorithm.

in the end it looks like alot of work but the outcome would definately be good.

MacBlender

Hi - thanks for going to look.
The way I see it, SDL is just a means to get dots on the screen - there is no reason to use SDL in the Blender plugin. The algorithm for the effect is in the C source-code on that SDL link I posted.

I saw a java plugin, called Anfy (I think), that had an even better version of the water effect - simply stunning and fast as hell even in Java! This was closed-source though, pity.

Anyway - the biggest hassle I can see at the moment is working out how many pixels across and down the given texture is going to be.
I don’t think I am grokking the fundamental principals behind textures - it would appear that they scale seamlessly to fit any size area, and this is confusing me at the moment.

I must study some of the plugin code more closely, see if I can get the message behind the code.

Ah well, any further info would be nice.

ciao.

yeah but by the looks of it there may be sdl calls int eh source so you can’t just port it over you’ll have to use the algorithm to build your own.

in blender the textures are pulled over the entire mesh so what you could do is use the size of the mesh to create a ratio based texture, get the bound box and use the x and y coordinates to get an x:y relationship and build your own texture on that.

MacBlender

How do I get the ‘bound box’ ? I have ordered the manual, but it hasn’t arrived yet - perhaps the info is in there?

goto www.blender.org and in the python documentation its

Blender.Object.Object.getBoundBox()

you use it like this


import Blender

obj = Blender.Object.GetSelected()[0]
obj.getBoundBox()

MacBlender

Ok - I can feel my brain starting to expand… use python to get the bounds and then …somehow… get that into a C texture plugin… brain is going to … BLOW!
:slight_smile:

I think I will try to base it on a ‘scale’ variable. The user can make the texture bigger or smaller until it fits as best as it can. I suppose this will mean dynamic arrays and in C that means pointers. Hoo boy - time for the headache pills.

Here is an old script I just updated fo 2.34 which does this effect on a mesh plane.
Just scriptlink it to a framechanged event (scene) and use alt-a in 3d view. It will both create and modify the mesh:


from Blender import Get, NMesh, Object, Redraw, Registry
from Blender.Noise import random

#############################################################

# Damp factor, the higher, the slower the waves fade out.
# If you set this higher than 1.0 (even a little bit),
# you will get chaos VERY fast,  possibly even a crash...
# When exactly 1.0, waves will never die out, also resulting
# in chaos, but less serious.
# Reasonable values are from 0.75 to <1.0.
damp = 0.9

# Mesh grid size, set this higher if you have a fast computer
# This runs reasonably smooth on my 1.4Ghz athlon.
# When using subsurf this needs to be set lower.
size = 50

# The number of frames to wait before another random
# drop of water falls onto the surface.
dropwait = 50

# The maximum initial bump height caused by the waterdrops.
max_ofs = 5.0

# the name of both the object & mesh
name = 'Waterplane'

#############################################################

size2 = size*size
resize_mesh = 0
bufs = Registry.GetKey('bufs')
if not bufs:
	print "new buffers"
	bufs = {}
	wbuf1 = [0.0]*size2
	wbuf2 = [0.0]*size2
	bufs['wbuf1'] = wbuf1
	bufs['wbuf2'] = wbuf2
	Registry.SetKey('bufs', bufs)
else:
	wbuf1 = bufs['wbuf1']
	wbuf2 = bufs['wbuf2']
	if len(wbuf1)!=size2:
		print "resizing buffers"
		wbuf1 = [0.0]*size2
		wbuf2 = [0.0]*size2
		bufs['wbuf1'] = wbuf1
		bufs['wbuf2'] = wbuf2
		Registry.SetKey('bufs', bufs)
		resize_mesh = 1

try:
	ob = Object.Get(name)
	me = ob.getData()
	new = 0
except:
	new = 1

if new or resize_mesh:
	me = NMesh.GetRaw()
	for y in range(size):
		for x in range(size):
			me.verts.append(NMesh.Vert(x-(size/2), y-(size/2), 0.0))
	vts = me.verts
	for y in range(size-1):
		for x in range(size-1):
			fc = NMesh.Face()
			fc.v.append(vts[size*y + x])
			fc.v.append(vts[size*y + (x+1)])
			fc.v.append(vts[size*(y+1) + (x+1)])
			fc.v.append(vts[size*(y+1) + x])
			fc.smooth = 1
			me.faces.append(fc)

if ((Get('curframe')-1) % dropwait)==0:
	# could make only a single bump/dent,
	# but a pseudo gaussian bump looks slightly better.
	x = int(size * random())
	y = int(size * random())
	xm = x-1
	if xm<0: xm=0
	xp = x+1
	if xp>=size: xp=size-1
	ym = y-1
	if ym<0: ym=0
	yp = y+1
	if yp>=size: yp=size-1
	y *= size
	ym *= size
	yp *= size
	v = max_ofs*random()
	wbuf1[x+y] += v
	v *= 0.5
	wbuf1[xm+y] += v
	wbuf1[xp+y] += v
	wbuf1[x+ym] += v
	wbuf1[x+yp] += v
	v *= 0.5
	wbuf1[xm+ym] += v
	wbuf1[xp+ym] += v
	wbuf1[xm+yp] += v
	wbuf1[xp+yp] += v


for y in range(size):
	ym = y-1
	if ym<0: ym=0
	yp = y+1
	if yp>=size: yp=size-1
	ym *= size
	yp *= size
	yy = y*size
	for x in range(size):
		xm = x-1
		if xm<0: xm=0
		xp = x+1
		if xp>=size: xp=size-1
		wbuf2[yy+x] = damp*(0.5*(wbuf1[ym+x] + wbuf1[yp+x] + wbuf1[yy+xm] + wbuf1[yy+xp]) - wbuf2[yy+x])
		me.verts[yy+x].co[2] = wbuf2[yy+x]

bufs['wbuf1'] = wbuf2
bufs['wbuf2'] = wbuf1

if new:
	ob = NMesh.PutRaw(me, name)
	ob.setName(name)
else:
	NMesh.PutRaw(me, name)
if (me.getMode() & NMesh.Modes.SUBSURF):
	ob.makeDisplayList()
Redraw()

print "Frame done
"

I did this a long time ago for a really old graphics demo I wrote in delphi which did this among other things, I used Hugo Elias tutorials which explains this effect very well:
http://freespace.virgin.net/hugo.elias/graphics/x_water.htm

It should be quite straightforward to make a texture plugin out of it, no python needed at all, you only would need to return the intensity and normal vectors calculated from the heightfield.

now then if you could only coordinate that with the falling of an object through the grid.

make some interesting water effects.

MacBlender

P.S. Eeshlo when are you gonna let out that cloth hack of yours?

Thanks for that code eeshlo. It reminded me that I did a similar mesh-wave thingum for Director 8.0 using that same web-link, I had forgotten that!

I am 100% new to 3D code and am struggling to make head or tales of even the simplest texture like “bricks.c”.

At the moment, my biggest puzzle is: how do you know the width and the height of the area that you will use as the heightfield? i.e. what are the dimensions of the array?
This effect needs edges - it seems textures work without knowing the edges - beats me how!

You can easily do that by setting the waterdrop position to that of another object, in turn you can then also alter the position of the object to make it follow the height surface modulations.

P.S. Eeshlo when are you gonna let out that cloth hack of yours?

I don’t have anywhere to put it online. Besides that, it was only really a one-weekend experiment that I did more than two years ago. I never did anything with it, mostly because it was too difficult to make it do the things I wanted it to do. It would have at least needed some sort of self-collision as well as general collision with other objects. Even getting simple sphere-triangle collisions right for the Dynamica particle system was difficult enough. I didn’t really feel like doing all that for cloth. I had made the beginnings of a python C++ collision module back then using Opcode, which worked quite well, but I never developped it much further than general mesh-mesh collision.
Anyway, because of all that, it is not much use other than maybe something for other coders to do something with it, so if you want it, send me a PM with your email address and I’ll send you the blendfiles.

You don’t need to know any area, grid size could be a user setting. It would only determine the resolution of the texture. I mean, if you wanted to put an image on a object as a texture in Blender, you don’t need to know the size of an object before you can choose a suitable picture, do you?
Blender sends you texture coordinates depending on what sort of mapping you set the texture to, and you use these to get the corresponding values from the heightfield.
Something that would be useful though is to calculate the texture so the changes would wrap around the 2d grid, that would make the texture tilable.
For the above python code, the buffer update segment would then look like this:


for y in range(size):
	ym = (y-1) % size
	yp = (y+1) % size
	ym *= size
	yp *= size
	yy = y*size
	for x in range(size):
		xm = (x-1) % size
		xp = (x+1) % size
		wbuf2[yy+x] = damp*(0.5*(wbuf1[ym+x] + wbuf1[yp+x] + wbuf1[yy+xm] + wbuf1[yy+xp]) - wbuf2[yy+x])
		me.verts[yy+x].co[2] = wbuf2[yy+x]

Note that in C/C++ you can’t use the modulo operator like this, since the result can be negative. in C, for the negative case you would have to do something like:


ym = (y-1) % size;
if (ym<0) ym += size;

or if you restrict gridsize to powers of two, you could use the & (and) operator instead.

If you are interested in cloth,

Zaz should hopefully have his new cloth simulator for Blender out soon (last I talked to him he was done, but had some last minute changes he wanted to do before a release… this was about a month ago i think?).

LetterRip

i was workign with zaz on his cloth for a while, i’m just interested in eeshlo’s version and soem of the things it involves.

MacBlender

eeshlo - thanks for the excellent information. You sound like an old hand at things 3D and C.

I will tinker-on with this, and hope I get somewhere soonish.

Cheers.

Hi, an update and some questions.

I have at least seen the ripples forming in the texture-preview! This was very cool, given my experience with C.

I have a problem when I render, however.

When I ran some tests, based on the texture-preview, I found that the texvec[0] ranged from -0.978723 to 0.978723.

When I look at those values during a rendering, they do not seem to line-up so nicely. They range all over the place and jump around.

I am (with my terrible maths) trying to stretch the (texvec[0],texvec[1]) values to a number between 1 and 200 (the size of my heightfield array - 200 by 200) so that I get an ix,iy value that can index the array.

So, using texvec[0]:
texvec[0] —> ix —> index —> result[0] = source[index]/255.0

  • is the basic plot, where “—>” means I do some calculation.

The problem is that the numbers for texvec[0] and [1] under a rendering are screwing me around. They seem to be different from the ones given under the texture-preview.

Where am I going wrong?
Can one of you brainiacs give me a low-down on texture co-ords and how they relate to a plugin?

I have included my code so far - be kind :slight_smile:
Thanks.

/* Texture plugin : Waves and ripples etc.
Donn Ingle

Ref: http://freespace.virgin.net/hugo.elias/graphics/x_water.htm
Ref: http://www.libsdl.org/projects/water/

Please forgive my coding and algorithms - I am not a programmer!

*/
 
#include "math.h"
#include "plugin.h"


/* ******************** GLOBAL VARIABLES ***************** */

char name[]= "Waves"; /*Didn't see this in Blender */

/* Subtype names must be less than 15 characters */
/*These seem to create 'group' buttons named "Bricks","Someshit" etc */
#define NR_TYPES   1
char stnames[NR_TYPES][16]= {"Waves"};

VarStruct varstr[]= {
  { NUM|INT,  "Age",           10, 0,  200,  "Run the sim to this age."}
};

/* The cast struct is for input in the main doit function
   Varstr and Cast must have the same variables in the same order */ 

typedef struct Cast 
{
    int Age;
} Cast;

float result[8];

/* cfra: the current frame */

float cfra;

/* My vars and stuff */
int firstRun=1; /* To allow pre-run stuff to happen */

/*How wide and large my array is */
#define WIDTH 200 
#define MAPSIZE WIDTH*WIDTH

/*The arrays and pointers to them */
int array1[MAPSIZE];
int array2[MAPSIZE];
int *swapper, *dest, *source;

void doWave(); /*The func that draws the waves */
void zero(); /*zero the arrays */


int plugin_tex_doit(int, Cast *, float *, float *, float *);


/* ******************** Fixed functions ***************** */

int plugin_tex_getversion(void) 
{   
   return B_PLUGIN_VERSION;
}

void plugin_but_changed(int but) 
{
printf("change
");
firstRun = 1; /* allow the pre-run stuff to happen again. */
zero(); /* Zero the arrays to clear them of old waves. */
}

void plugin_init(void)
{
    /*plugin_init called only once on very first run */
    //printf("plugin_init
");
    /* printf("cfra = %f
",cfra);  cfra is not available here */
    //zero();
    //printf("plugin_init ends
");
}

/* Zero the arrays to clear them of old waves. */
void zero(){
    int i;
    for (i=0;i<MAPSIZE;i++) {
        array1[i] = 0;
        array2[i] = 0;
    }
}


/* this function should not be changed: */

void plugin_getinfo(PluginInfo *info)
{
   info->name= name;
   info->stypes= NR_TYPES;
   info->nvars= sizeof(varstr)/sizeof(VarStruct);
   
   info->snames= stnames[0];
   info->result= result;
   info->cfra= &cfra;
   info->varstr= varstr;

   info->init= plugin_init;
   info->tex_doit=  (TexDoit) plugin_tex_doit;
   info->callback= plugin_but_changed;
}

/* Stolen code - total mystery, will apply mind later.... */
void doWave() {
    int i;
    
    for (i=WIDTH; i<MAPSIZE-WIDTH; i++)
    {
        dest[i] = (
                    ((source[i-1]+
                        source[i+1]+
                        source[i-WIDTH]+
                        source[i+WIDTH])  >>1) )        -dest[i];
    
        dest[i] -= (dest[i] >> 5);
    }
    /*Swap pointers to buffers! Thanks Mike! */
    swapper = dest;
    dest = source;
    source = swapper;
}


int plugin_tex_doit(int stype, Cast *cast, float *texvec, float *dxt, float *dyt)
{
float u,v; /* To hold the tex coords */
int ix,iy,index; /* Various indexes into arrays */
int r; /* An for loop var */

/* Get the (u,v) coords */
u = texvec[0];
v = texvec[1];
      
/* Check for pre-run flag - do stuff just once */
 if (firstRun==1) {
     
     printf("The waves plugin is preparing the height-map.
");
     /*Set the pointers to point to the arrays*/
     source=array1;
     dest=array2;
     
     /* Drop a drop of water into the heightmap *?
     /* (c*w)+r = i */
     for (r=0;r<10;r++){
     //index=((rand()%WIDTH+1)*WIDTH)+(rand()%WIDTH+1);
     index = 1000 + (r * 10);
     source[index] = 255; //Set a value
     }
     //printf("got here ok
");
     
     /* run the simulation X number of times to get a visible effect */
     zero();
     printf("Running sim %d times
",cast->Age);
     for (r=0; r < cast->Age; r++) doWave();
     
     firstRun=0; /*No more runs */
     printf("The waves plugin is finished preparing the height-map.
");
     
 }
            
//printf("u is : %f
",u);

/* The basic plot is to take the values in the source[] array 
 * and to use the texvec[0] and [1] to access them
 * and then convert the given value to between 0 and 1
 * I sense a lot of maths coming up!
 */
 
 /* My half-ass attempt at converting the u into an ix */
 ix = (int) 100 * (1+u);
 iy = (int) 100 * (1+v);
 
 index=(ix*WIDTH)+iy; /* index into a 1D array */
 //printf("source[index]=%d",source[index]);
 //printf("index=%d
",index);
 
 /* This shows wild results on render F12 */
 /* But is fine under texture preview */
 printf("(ix,iy):%d,%d
",ix,iy); 
 
 //if (source[index]>0) {
  //printf("At %d,%d source[%d]=%d
",ix,iy,index,source[index]);
  //}
 
 /* Get the data and then try to convert it into range 0 to 1 */
 /* My maths is crap. I struggle to ratio numbers together */
 result[0]=(float)source[index]/255.0;
 
 if (result[0]>0) printf("result[0]:%f
",result[0]);
 if (result[0]>1) result[0]=1;
 
 //result[0]=1;
 
 /* RGB */
 result[1]=0;
 result[2]=0;
 result[3]=1; 
 
 /* Alpha */
 result[4]=1;
 
 /* Some total mystery */
 result[5]=0;
 result[6]=0;
 result[7]=0;
 
 return 0;
  
}


Well, there are several things you could improve, but not really important at the moment. One problem is that the values you get are not clamped, you have to make sure you don’t read any value beyond the array memory, which can contain anything. So, for both the x and y values, make sure they are in range. This does not necessarily mean that you have to work out some formula that precisley maps the blender coordinates to x and y in the array (the mapping could be set to ‘global’ for instance, which will return quite different results) just that you make sure that values don’t get less than 0 and not larger than width-1. Like in the above python code.

Ok, I don’t want to bug you, but can you be more specific about how I can map the values I get from texvec[ ] into my array?
It seems that the formula will change depending on the map-to setting in materials…

I will look at your Python code, but I have it in my head that it’s operating on the nodes of a mesh - this is a nice and predictable number of items, counted from 0 up to X.

With the textures, the picture is very messy and it’s too fuzzy for me to make sense of. I don’t want to give up for lack of information!

Once the mapping works, I will look at making the array dynamic (a ‘resolution’ slider) and make the effect look a lot better.

Did you compile and run the code at all?

Thanks for any help.
:wink: