|Photos||Blog||Random Things||Links||Guest Book||Resume|
Realistic Water Rendering, by Yann LApr 23, 2003
Times are in EST and are about 4 minutes fast
[18:04] <cppman> 6:00
[18:04] * trent sets mode: +m
[18:04] <trent> Alright guys, welcome to #Lecture.
[18:04] <trent> Tonight, we have Yann Lombard giving a lecture on "Realistic Water Rendering." But first, I must go through the rules.
[18:05] <trent> First of all, the channel will be muted to all but Yann, RedBeard and myself. If you want to ask a question, /notice one of us, and we'll decide to make it public or not.
[18:05] <trent> Second of all, if you want to discuss the lecture while it's going on, please join #BackOfTheClass.
[18:06] <trent> And, other than that, enjoy the lecture, and if you want to give a lecture of your own, please contact RedBeard or myself, and we'll give the hook-up.
[18:06] <trent> And, with that said, here is Yann. :)
[18:06] <Yann> Alright, welcome to "Realistic water rendering"
[18:07] <Yann> Water has always been one of those WOW-effects, an important eye-candy in any modern 3D game
[18:07] <Yann> But unfortunately, water is a pretty complex effect, especially, if you are trying to achieve a realistic water simulation
[18:08] <Yann> The topic of water simulation and rendering is very vast, as you might imagine, so we will probably only scratch the surface
[18:09] <Yann> But at the end of this lecture, you will be able to implement a nice looking water surface, with realistic physically base movements.
[18:09] <Yann> OK, I will divide the lecture into two distinct parts:
[18:09] <Yann> 1) The water wave model
[18:09] <Yann> 2) Rendering the water
[18:10] <Yann> We will open the channel for discussion between both parts, and I can get something to drink ;)
[18:10] <Yann> So, part 1: how to create the waves in the first place, how to represent the water
[18:11] <Yann> We basically have three major possibilities: 2D, 3D, or 2.5D
[18:11] <Yann> 2D water surfaces have been used in most older games, for example the quake trilogy
[18:11] <RedBeard> excuse me while i just drop a little info into the topic
[18:12] <Yann> Being only a simple polygonal surfaces, with animated textures, it is fairly easy to implement, and very fast
[18:12] <Yann> But it is far from convincing
[18:12] <Yann> So we have the other extreme: full 3D water.
[18:13] <Yann> Here, we represent the whole water body (typically, using a 3D voxel grid), and work on that 3D data set
[18:14] <Yann> The advantage is highest realism, especially if we use a Navier Stokes solver (more on this later)
[18:14] <Yann> The obvious drawback, however, is the extreme computational power required to solve complex differential equations over a full 3D voxel grid.
[18:15] <Yann> But there is a middle way between both: a 2.5D representation.
[18:15] <Yann> In a typical game, we are more interested in the movement and animation of the water surface, we rarely need a full 3D simulation
[18:16] <Yann> And this is where we can do our first approximation: we will represent our water body as a heightmap,
[18:16] <Yann> as you surely know them for terrain rendering.
[18:16] <trent> :)
[18:16] <Yann> trent's the pro with that ;)
[18:17] <Yann> So, using such a heightmap, we can give volume to our water body, but restrict most cojmputations to 2D.
[18:17] <Yann> This will considerably decrease the required CPU cycles to compute the simulation, and such increase our framerate.
[18:18] <Yann> Now, a wide range of different ways to compute the water heightmap exist
[18:19] <Yann> ranging from simple to extremely complex, we will shortly cover the properties of the following methods, while taking a deeper look into the last one:
[18:19] <Yann> * Perlin noise
[18:19] <Yann> * FFT (Fourier synthesis)
[18:19] <Yann> * NSE (Navier Stokes equations) and variants
[18:20] <Yann> The easiest algorithm is Perlin noise.
[18:21] <Yann> It is a nice looking procedural function, able to create mutli-frequency noise patterns, and is pretty well suited to represent large water bodies, such as oceans.
[18:21] <Yann> The problem with Perlin noise, is that you cannot control it very well.
[18:22] <trent> What about it is the hardest to control?
[18:22] <Yann> The only thing you can actually do, is changing the wave amplitude and frequencies. but that's not enough for a really flexible simulation.
[18:23] <Yann> Still, it can be a visually appealing solution, and might be worth a try, if this is your first attempt at water simulation.
[18:23] <RedBeard> so it can't respond to people falling in the water and stuff like that?
[18:23] <Yann> Correct.
[18:24] <Yann> You can get more information about (fake) Perlin noise at this site: freespace.virgin.net/hugo.elias/models/m_perlin.htm
[18:24] <Yann> Second, we have FFT.
[18:24] <trent> Another perlin noise link: mrl.nyu.edu/~perlin/
[18:25] <Yann> Right, that's the 'real' Perlin noise, although it is a bit slow for realtime applications.
[18:25] <Yann> So, just as Perlin noise, FFT is not physically based. It is a statistical simulation of ocean and large water body waves.
[18:26] <Yann> The waves are composed by combining several sinusoidal wave forms of differing amplitudes, frequency and phase.
[18:26] <trent> Yann: we have a question from someone: "What is the difference between 'real' and 'fake' perlin noise?"
[18:27] <Yann> Fake noise approximates the visual aspects of Perlin noise by adding pre-filtered (often bilinear) 2D textures.
[18:27] <Yann> It has not the same smooth and fully random properties, but it is a quick and fast to compute replacement.
[18:28] <Yann> Back to the FFT. The wave combining results in long (possibly intersecting and splashing) waves, following a general direction,
[18:28] <Yann> the direction of the wind, for example.
[18:29] <Yann> It is better controlable than Perlin noise, as one can specify basic wind conditions, that will directly affect the direction and speed of the waves.
[18:29] <Yann> But it is still not able to simulate complex wave interaction, such RedBeard pointed out above.
[18:30] <Yann> From a technical side, FFT uses the (discrete) inverse fast fourier transform to convert a given set of phases and amplitudes into
[18:30] <Yann> a combined wave representation.
[18:31] <Yann> The resulting waves will be very smooth, and round at the top. This is not always desirable.
[18:31] <Yann> Various methods to add sharpness to the waves exsit, making them look more choppy.
[18:32] <Yann> Should you be interested in implementing an FFT based system, I suggest reading the Paper "Deep Water Animation and Rendering" by Jensen & Golias.
[18:32] <Yann> At last, but not least, we have the NSE (Navier Stokes Equations).
[18:33] <Yann> In their original (continuous) form, they are an accurate tools to describe the motion of an incompressible fluid such as water.
[18:34] <Yann> Closed form solutions for the NSE are very complex (and do not always exist), so we need to solve them over a discrete domain,
[18:34] <Yann> for example a voxel grid.
[18:35] <trent> [18:31] -CoffeeMug- I think it's important to point out that FFT and perlin noise may be superior solutions for simulating large bodies of water because they land themselves to LOD algorithms, while NSE solvers do not.
[18:35] <Yann> That is right, although both can be combined.
[18:36] <Yann> The real advantage of the NSE (for local and small water surfaces), is that they can accuractely simulate direct interaction with the environment.
[18:36] <Yann> That is an important feature for a game, eg. when the player performs actions while standing in the water.
[18:37] <trent> [18:33] -Drevay|GS- like, in metroid prime? (ever so good water interaction)
[18:37] <Yann> So in summary: for near water surfaces, NSE offer the best quality, but they are also the most performance hungry of the three.
[18:38] <Yann> Sorry, I never played metroid prime, so I can't really comment :)
[18:38] <trent> I hated the game. :)
[18:38] <Yann> Essentially, use NSEs wisely.
[18:39] <Yann> OK, trent wants a little pause, server probs...
[18:40] * RedBeard sets mode: -m
[18:40] <B2K|Ash> wow
[18:40] <trent> ...
[18:40] <Yann> Ah, back again :)
[18:40] <Zith_> bam
[18:40] * trent sets mode: +m
[18:41] <Yann> OK, so my last words were: use NSEs wisely, but again, we have approximations that can make them faster.
[18:42] <Yann> As a small sidenote: when trying to strip down an NSE, you have to keep in mind that numerical methods tend to get unstable.
[18:42] <Yann> With the wrong parameters, or a large timestep, your simulation can easily 'blow up'.
[18:42] <Yann> That can be fixed by using more precise integrators, but those are more expensive, again.
[18:43] <Yann> I will now present a stripped down version of the NSE, while being very short and easy to understand,
[18:44] <Yann> it still allows highly realistic water animation and interaction.
[18:44] <Yann> As I mentioned above, we can simplify the NSE even more, by ignoring some physical properties of the fluid body (eg. viscous forces).
[18:45] <Yann> We can simply represent the water body by using basic hydrostatic pressure differentials. Despite the rough approximation of the realworld model,
[18:46] <Yann> we can get impressive results with only a few lines of code.
[18:46] <Yann> You can download a basic implementation at membres.lycos.fr/ylmb/source/solver.cpp + membres.lycos.fr/ylmb/source/solver.h
[18:48] * trent sets mode: -m
[18:48] <CoffeeMug> we can talk now? :)
[18:49] <[C4]Socrates> all but you
[18:49] <Yann> OK, who wants a mathemtical description of that algorithm ?
[18:49] <Drevay|GS> :O
[18:49] <[C4]Socrates> bring out the math
[18:49] <cppman_> meee;)
[18:49] <Drevay|GS> *raises hand* me
[18:49] <masonium> me
[18:49] <EchoQ> me
[18:49] <Yann> OK, just wanted to make sure it wouldn't bore you ;)
[18:49] <Eldrik> me
[18:49] <B2K|Ash> me
[18:49] <B2K|Ash> i think
[18:49] <polestar> _o/
[18:49] <Zordan> I would like to see some screenies
[18:49] <Etran1> I do
[18:49] <Etran1> I want the math
[18:49] <vox> both please
[18:49] <ga> me
[18:50] <CoffeeMug> like a bunch of babies :)
[18:50] <trent> [18:46] <Sam> trent will this lecture cover any underwater rendering, or just surfaces?
[18:50] <Yann> OK, well I didn't have the time to take some screenies, but just try out the algorithm :)
[18:50] <Drevay|GS> ok..
[18:50] <Yann> trent: we will only cover surfaces for now, or thge lecture might take a couple more hours
[18:51] <Sam> ;'(
[18:51] <Yann> OK, so here comes the math...
[18:51] * vox ducks
[18:51] <Yann> heh :)
[18:51] * trent sets mode: +m
[18:52] <Yann> Imagine a heightfield, where each voxel represents a column of water from the bottom to the surface.
[18:53] <Yann> The static pressure P(ij) on a water column at position (i,j) is given by the following equation: P(i,j) = h(i,j)*p*g + p0 + E(i,j)
[18:54] <Yann> where p is fluid's density, a the gravitational acceleration, and p0 the pressure of the atmosphere.
[18:54] <Yann> Being constant within the entire system, we can ignore the atmospheric pressure p0.
[18:55] <Yann> E(i,j) is an external force we can apply to the water surface, which is crucial for the interaction between the user and the water surface.
[18:56] <Yann> Now, we need to somehow connect the individual water columns, to allow the water to flow from one column to the next.
[18:56] <Yann> We do that by creating a network of virtual "pipes", that connect each column with its 8 neighbours:
[18:57] <Yann> left and right neighbour on the horizontal, top and bottom on the vertical, and the four diagonal connections.
[18:57] <Yann> Those pipes will allow the water pressure to be distributed over the entire system, in a controlled way.
[18:58] <Yann> This pressure propagation is the cause of the water waves we are used to see in the real world.
[18:58] <trent> [18:55] -CoffeeMug- is P(i, j) a vector or a scalar?
[18:59] <Yann> P(i, j) is a scalar: the pressure at grid position (i,j) in the voxel field.
[18:59] <Yann> It is best represented by a float (for speed reasons).
[19:00] <trent> Alright, just a quick tweak on my part.
[19:00] <Yann> OK, to recap that: at the end of each pipe, you have two columns. The water in those columns will put a pressure on each pipe end.
[19:00] <trent> [18:57] <buggle> but isn't E(i,j) a vector? (it's a force)
[19:01] <Yann> E(i,j) is also a scalar: it is the force applied to the water column at (i,j). The application is assumed from above.
[19:01] <RedBeard> i'm guessing it's a normal force, which impacts the vertical motion of the surface
[19:01] <Yann> That's right.
[19:02] <Yann> You cannot apply a diagonal force to a heightfield voxel, that's a drawback of the approximation.
[19:02] <RedBeard> so how might you represent a person running his hand through the water? just kinda hack it with vertical forces?
[19:02] <Yann> The next step is to compute the acceleration of the water through the pipe.
[19:03] <Yann> You will apply vertical forces on the voxels, as the hand passes through, yes.
[19:03] <Yann> To get non-vertical force application, you will have to revert to a full 3D voxel grid (volumetric approach).
[19:04] <Yann> So, back to the pipe accelerations.
[19:05] <Yann> We can compute it by plugging the the pressure differential between both columns (i,j) and (k,l) into the well known equation F = m*a
[19:06] <Yann> If we solve for a, we get: a(i,j -> k,l) = c * dP(i,j -> k,l) / m
[19:07] <Yann> where c is the cross-sectional area of the pipe, and m the mass of the fluid in the pipe.
[19:07] <Yann> We can compute the mass as m = p*c*l, l being the pipe length (and p the density, remember from above ?)
[19:08] <Yann> OK, now we have the acceleration, we can integrate it over a given timestep to yield the flow through the pipe, as well as the change of volume in the columns.
[19:09] <Yann> For the sake of simplicity, we will assume constant acceleration during that time step, and we will use a basic Euler integrator.
[19:09] <Yann> We get:
[19:10] <Yann> Flow(i,j -> k,l)(t + dt) = Flow(i,j -> k,l )(t) + dt(c*a(i, j -> k,l))
[19:10] <Yann> And:
[19:11] <Yann> DeltaVolume(i,j) = dt * SUM(grid)(Flow(i,j -> k,l)(t+dt))
[19:11] <Yann> We get an updated column volume from that, but we need a height.
[19:12] <Yann> It can be easily reconverted to a height, by simply dividing the volume by the column's base area.
[19:12] <Yann> Well, that's pretty much all we need for our simulation
[19:12] <Yann> We can summarize the high level structure of our simulator as follows:
[19:13] <Yann> * initialize all pipe flows to zero at startup (the fluid levels are balanced out)
[19:13] <Yann> * pour an initial amount of water volume into the system, distribute it equally amongst the columns.
[19:13] <Yann> For each time step:
[19:14] <Yann> * Update all pipe accelerations and flows as described above.
[19:14] <Yann> * Update the column volumes and heights, according to the new pipe flows.
[19:14] <Yann> The last thing you need to keep an eye on, is volume conservation.
[19:15] <Yann> We *have* to make sure, that under normal circumstances, no water exists or enters the system, it is a closed system simulation.
[19:15] <trent> -cppman_- How do we deal with simulation when the sea floor is not a flat surface, and heights mean less
[19:16] <trent> [19:12] -cppman_- not mean less, but don't mean the same thing for each column
[19:16] <Yann> The height we get from the simulator is based on a constant bottom height.
[19:17] <Yann> But that's only for the sake of the computations.
[19:17] <trent> -CoffeeMug- wouldn't you have to initialize pipe flows to some random values? It seems like the water wouldn't ever move otherwise.
[19:17] <Yann> You can very well have a non-flat water floor.
[19:17] <Yann> CoffeeMug: correct. That's the initial state of the system.
[19:17] <Yann> Everything is balanced out.
[19:18] <Yann> Until you apply an external force through E(i,j)
[19:18] <Yann> *Every* water motion is triggered by an external force, might it be wind, or a characters hand flowing through the water.
[19:19] <Yann> When no external force is applied anymore, the system will slowly come back to it's intial state of balance.
[19:19] <trent> -Rajansky- So do you have to come up with some sort of wind model if you want to simulate an ocean surface? (which ripples)
[19:19] <Yann> Assuming we add loss of energy due to water friction.
[19:20] <RedBeard> -[C4]Socrates- if the forces on the columns are assumed to be normal, how would you do wind?
[19:20] <Yann> Otherwise, it will enternally continue it's movement, and that's not very realistic ;)
[19:21] <Yann> You can model wind by using a noise function 'blowing' over the mesh, and distributing forces in the direction the wind is blowing.
[19:21] <Yann> As I mentioned, you can see a basic implementation of the algorithm here: membres.lycos.fr/ylmb/source/solver.cpp
[19:22] <Yann> As you can notice, it's much smaller than one might think after this math ;)
[19:22] <Yann> It is a modified version of the approach described in the paper "Dynamic Simulation of Splashing Fluids" by O'Brien + Hodgins.
[19:22] <Yann> You might want to take a look for more details and math.
[19:23] <Yann> Although I would not recommend using the volume conservation approach discussed in that paper, it is not very efficient.
[19:23] <Yann> OK, you reached the end of part 1.
[19:23] <Yann> Part 2 will be about rendering the whole thing.
[19:24] * RedBeard sets mode: -m
[19:24] <RedBeard> free-form question & discussion time during the break
[19:24] <wannabecoder> interesting
[19:24] <Yann> thanks
[19:24] <polestar> Thanks to you Yann
[19:25] <B2K|Ash> cool
[19:25] <B2K|Ash> i didnt understand a word yann, but im sure if i was inteligent it would be good stuff
[19:25] <Yann> lol, I hope the math hasn't scared too many people away...
[19:25] <ash> math what math
[19:25] <ash> :P
[19:25] <HangDude> thx Yann. the math part was pretty clear to me in the end. I just wondered on what timesteps you would run such simulations ?
[19:25] <[C4]Socrates> we just skip over that
[19:25] <phantom> i just about followed the maths personaly ;)
[19:26] <Zeno> Yann - Could you be a bit more specific about how you would make wind-waves when you are restricted to pushing the water in an up-and-down motion?
[19:26] <ash> i understood the perlin noise par t thats it :P
[19:26] <Yann> HangDude: depends on the speed of your game. You should asjust the timestep to your framerate.
[19:26] <trent> Yann: Part 1 took up about 1.3 hours. :)
[19:26] <Yann> Zeno: OK. You simply push the water columns according to some wind function, one after the other.
[19:27] <Yann> trent: really ? damn, you're right ;)
[19:27] <wannabecoder> i wasnt here for the whole thing- and i dont think my 7th grade math education would help me much with what you are talking about
[19:27] <ash> Whats "hydrostatic pressure differentials"
[19:27] <Rubin> so the wind is more like a big steam roller?
[19:27] <Yann> I'll make part 2 a little shorter then...
[19:27] <trent> Yann: No need to.
[19:27] <trent> We're not on a timeline
[19:27] <Yann> OK
[19:27] <EchoQ> lets keep it long
[19:27] <polestar> it's 2:23 am here
[19:27] <BlabberBoy> long is good :)
[19:28] <aniero> hehe
[19:28] <gooball> :)
[19:28] <Zeno> Hmm, but for wind-based waves, you want a nice distribution of wave frequencies. It seems like, to do what you say, you'd almost have to know what the waves look like beforehand?
[19:28] <Yann> Rubin: yeah, if you want. Have a look at this: w3imagis.imag.fr/Publications/2001/PC01/
[19:28] <aniero> yes, keep it long.
[19:28] <posit> presumably, given a horizontal window flow, vertical force could be approximated by a simple dot product with surface normal...
[19:28] <posit> *wind, not window :)
[19:28] <Yann> The paper I linked to is for grass animation through wind, but it's applicable the exact same way to water
[19:28] <buggle> maybe I missed this cause I joined partway through, but when you say volume conservation are you talking about boundary conditions such that the flow in/out of the system is 0?
[19:28] <[C4]Socrates> i'm gonna make a java demo of this because i am so inspired
[19:28] <Yann> You can even have tornados ;)
[19:28] <Rajansky> oh sweet, nice paper...
[19:29] <noisecrime> do the fuild levels have to be equal in all the coulmns at the start?
[19:29] <vox> neat :)
[19:29] <RedBeard> buggle: yes, i think so
[19:29] <ProgrammerDave> Yann, is there a demo to this?
[19:29] <aniero> really?
[19:29] <buggle> thanks
[19:29] <Yann> buggle: yes, but more accuracy problems, we lose accuracy by our simplified finite differentiation.
[19:29] <aniero> i think having nonuniform distributions at the outset would just start your system off noisily, and it would eventually settle back into an equilibrium (given a friction-loss constant)
[19:29] <RedBeard> noisecrime: you could probably initialize them to whatever you want, it just might look a little funky when it starts up
[19:29] <Rajansky> you could prolly whip up a demo real quick if you want to... it looks like Yann's code is really easy to use... I think you just call Simulate() each frame, right? And ApplyForce every time you need to...
[19:29] <Sages> Yann: have you ever tried Cellular Automation for water representation
[19:29] <buggle> ah I see
[19:30] <Yann> Rajansky: yes.
[19:30] <Yann> Sages: only for clouds fluid dynamics. Not yet for water, but it could prove interesting.
[19:30] <ProgrammerDave> good, then I'll have to view the logs because I missed most of the lecture
[19:30] <Sages> Yann: I bet it would be
[19:30] <Sages> Yann: and you could get realistic water interaction with it
[19:31] <Yann> Probably, if implemented the right way.
[19:31] <RedBeard> Yann: is there something you can do to simulate fluid displacement? like if you drop a big rock into a pond, the overall water level would rise because the rock displaces some volume
[19:31] <gooball> wow
[19:31] <CoffeeMug> Yann: do you think it's possible to hack LOD into the NSE solver? For instance, when an area of water is sufficiently far away you could combine 4 columns and increase the radii of the pipes
[19:31] <buggle> that's the point of the E(i,j) term isn't it?
[19:32] <Yann> Red: Yes, you can. The displacement itself is already modelled, you only need to increase the overall volume in the system.
[19:32] <aniero> CoffeeMug: just make sure you don't just multiply the pipe diameter by 4
[19:32] <Yann> Coffee: yep, that would be the next thing to include into a basic slover.
[19:32] <aniero> CoffeeMug: i would assume your idea would work
[19:32] <Yann> *solver
[19:32] <noisecrime> off topic-ish, but how would you mix reflection/refraction on the water surface -i know there is a formula for this, just wondering how it would be applied -per vertex/polygon via alpha map perhaps?
[19:33] <Yann> That's in part 2 ;)
[19:33] <noisecrime> cool
[19:33] <CoffeeMug> yann: it seems fairly simple, why do all the resources I looked at seem to say it's impossible to have LOD with NSE solvers?
[19:33] <Yann> OK, just a second, I'm going to get myself a whiskey-coke :)
[19:33] <ash> :-O
[19:33] <vox> so thats what gods drink..
[19:33] <wannabecoder> lol yann
[19:33] <RedBeard> heh, nothing like a plastered lecturer :P
[19:33] * ash whaps vox
[19:33] <Rajansky> haha
[19:34] <Yann> :)
[19:34] <phantom> i prefered being a plastered student ;)
[19:34] <buggle> "<Yann> so when rendering the surface of the whiskey^H^H^H^H^H liquid"
[19:34] <aniero> haha
[19:35] <Sages> lol buggle
[19:35] <Yann> OK, back.
[19:35] <Kyardan> this may be part 2..but how much of each method can be done on the gpu?
[19:35] <RedBeard> alright, ready to continue, yann?
[19:35] <Yann> Yep, I'm ready if you are
[19:35] * RedBeard sets mode: +m
[19:36] <Yann> So: part 2, the rendering.
[19:36] <Yann> Whatever wave model you chose, Perlin noise, FFT, NSE or something entirely different (cellular automation :)),
[19:36] <Yann> you'll end up with a heightfield, representing the water surface.
[19:37] <Yann> And that heightfield needs to be rendered.
[19:37] <Yann> From a geometrical point of view, the process is easy: just as you would render a heightmap terrain,
[19:38] <Yann> you form quads (or better: two individual triangles) from each 2*2 grid vertex set.
[19:38] <Yann> For optimal performance, you would probably use tri-strips for the water surface.
[19:39] <Yann> (Or even better: indexed tri-strips)
[19:39] <Yann> So now we have our water surface on the screen, but it still looks pretty boring, flat shaded with some constant colour ;)
[19:40] <Yann> And at this point, we will have to make some major decision:
[19:40] <Yann> Are we rendering a large water body, such as the sea, or more a small lake with lots of local geometry around ?
[19:41] <Yann> We can of course combine both, but that's not necessarily an easy task...
[19:41] <Yann> Both models work along the same optical laws in reality, but have to approximated differently on our GPU.
[19:42] <Yann> If we want to maintain the illusion of a realistic water surface, we need to take those optical properties into account.
[19:43] <RedBeard> -[C4]Socrates- does each point in the coordinate space have to represent a small 2d space (you'd get a bargraph if it weren't dense) or could they represent sparse points?
[19:43] <Yann> Being very complex is the real-world, we cannot hope to accurately simulate all of them, even less in realtime.
[19:44] <Yann> From the simulation, each point is located at the top of a water column, exactly in the middle.
[19:44] <Yann> And thus, represents a small 2d area.
[19:44] <Yann> But you don't really need to take those details into account while rendering, you can simply treat them as individual sparse points.
[19:45] <Yann> So, here again, we unfortunately need to approximate again.
[19:45] <Yann> But before we dive into the depths of pixel and vertex shaders, we forgot an important thing: the normals.
[19:46] <Yann> We have a heightmap, but where do we get the normals from ?
[19:46] <Yann> Simple answer: from the heightmap.
[19:46] <Yann> The usual (and fastest) way to compute normals for a heightfield in realtime, is finite differencing.
[19:47] <Yann> This is a very common algorithm, so I won't get into the derivation of it now. you can easily find additional information on the net.
[19:48] <Yann> For your convenience, here is a code snippet to create normals from a heightfield: membres.lycos.fr/ylmb/source/normals.cpp
[19:48] <trent> [19:44] -CoffeeMug- does the vertex buffer have to be updated every frame?
[19:49] <Yann> Yes, there is unfortunately no way around that. Other than implementing an NSE on the GPU :)
[19:49] <Yann> But you don't need to update the index buffer.
[19:50] <Yann> Now that we have all data required, let's have an (imaginary) look at a water surface:
[19:50] <Yann> What strikes us at first ?
[19:50] <Yann> * reflections.
[19:50] <Yann> * refractions.
[19:50] <Yann> * Lots of shiny little sparkles, if the sun is shining.
[19:51] <Yann> On a closer look, we will also notice, that water actually exhibits some diffuse properties.
[19:51] <RedBeard> <posit> have you looked into all-GPU algorithms, and if so how many features can you reproduce?
[19:52] <Yann> You cannot produce an entire NSE solver on the GPU. Not yet.
[19:52] <Yann> But you can actually approximate a simple noise algorithm.
[19:52] <Yann> First, let's look at reflections, since they are one of the most predominant properties of water.
[19:53] <Yann> If you look at the ocean, you'll notice that reflections seem very blurred, almost unrecognizable.
[19:53] <Yann> But on a local small water pool, with some objects around, you'll see clear reflections in the water, distorted by the waves.
[19:54] <Yann> Both approaches can be simulated, but we need different approaches.
[19:54] <Yann> *techniques, I meant ;)
[19:55] <Yann> In the middle of the sea, there is not much that can be reflected by the water. The sky, at most. Being a large scale object, we can encode
[19:55] <Yann> the incident light from the sky into a cubemap.
[19:56] <Yann> At each vertex of the water surface, we compute the reflection vector, according to the current viewpoint, and use it to index the cubemap.
[19:57] <Yann> while this technique is well suited for far-away environmental reflections, it is absolutely not useable for local geometry.
[19:57] <Yann> Let us recall a reflection algorithm most of you will know, for a second:
[19:57] <Yann> the good old stenciled reflections.
[19:58] <Yann> you have a two pass rendering approach: on the first pass, you reflect the scene around the reflection plane,
[19:58] <Yann> masking out the reflective region by the stencil buffer,
[19:58] <trent> [19:55] -CoffeeMug- is the cubemap generated at realtime? Which viewpoint do you choose to render the geometry into a cubemap?
[19:58] <Yann> and on the second pass, you render the scene with it's normal orientation.
[19:59] <Yann> Coffee: no, the cubemap is pregenerated (or updated, once the sky changes).
[19:59] <Yann> You don't need a specific location, since the cubemap will only encode far away geometry (which is assumed infinitely far away).
[20:00] <Yann> Think about a skybox: the projection is also assumed to be at inifinity.
[20:00] <Yann> Back to our planar reflections: we can use those to represent the local reflections in the water.
[20:01] <Yann> With a major difference: since water waves distort the reflection, we need to somehow manipulate them after they have been rendered.
[20:01] <Yann> So, instead of using the stencil buffer and rendering to the framebuffer in the first pass, we render the reflected geometry into a texture.
[20:02] <Yann> On the second pass, when rendering the water surface, we have the local reflections ready available in that texture we created.
[20:03] <Yann> The only problem is to figure out, how the texture will be applied to the water surface.
[20:03] <Yann> We can obviously not apply it the standard way (perhaps by stretching it over the water mesh), because it has been taken from a different coordinate frame
[20:04] <Yann> The whole camera view and projection is already encoded in that texture.
[20:04] <Yann> But of course, we have something that can save us here: projective textures.
[20:04] <Yann> We simply 'project' the texture onto the water mesh, just as if we had a slide projector shining onto the water.
[20:05] <Yann> Basically, we create texture coordinates that mimic the exact same camera view transformations and projections, as performed on the vertex positions.
[20:05] <Yann> That way, our texture will be optimally aligned on the water surface, as we will be mapping it in the correct coordinate space.
[20:06] * Quits: Yann (YannL@rennes-1-a7-62-147-99-189.dial.proxad.net) (Read error to Yann[rennes-1-a7-62-147-99-189.dial.proxad.net]: Connection reset by peer)
[20:06] <RedBeard> hmm
[20:06] * RedBeard sets mode: -m
[20:07] <gooball> hm
[20:07] * Joins: Yann (YannL@rennes-1-a7-62-147-99-189.dial.proxad.net)
[20:07] <RedBeard> i hate when that happens
[20:07] <CoffeeMug> Yann: why is it necessary to use two different methods? For large water bodies, couldn't you use the same method only with a smaller texture?
[20:07] <RedBeard> wb, Yann
[20:07] <Yann> wow. nice...
[20:07] <RedBeard> last message we recieved was : [20:05] <Yann> That way, our texture will be optimally aligned on the water surface, as we will be mapping it in the correct coordinate space.
[20:07] <Yann> What was the last words from me that came through ?
[20:07] * RedBeard sets mode: +m
[20:09] * RedBeard sets mode: -m
[20:09] <RedBeard> problems?
[20:09] <Yann> ?
[20:10] <posit> op Yann
[20:10] <RedBeard> ohh, silly me :P
[20:10] <masterg> .op Yann
[20:10] * X2 sets mode: +o Yann
[20:10] <BlabberBoy> heh
[20:10] <Yann> oops
[20:10] <Graem[Sleep]> *applause*
[20:10] * RedBeard sets mode: +m
[20:10] <Yann> test, can you hear me ?
[20:11] <Yann> :)
[20:11] <RedBeard> affirmative
[20:11] <Yann> OK then :)
[20:11] <Yann> For more informations about projective textures, have a look here: developer.nvidia.com/view.asp?IO=Projective_Texture_Mapping
[20:11] <Yann> Having done that, we will notice, that our water is a perfect flat mirror, and that the waves will not affect the reflection in the slightest way.
[20:12] <Yann> We need to somehow distort the projective texture coordinates, so that they reflect the way the reflection vector changes over the water surface.
[20:12] <Yann> Just as we did with the cubemap before.
[20:13] <Yann> But unfortunately, while the cubemap was in worldspace, our projective reflection texture is not.
[20:13] <Yann> And the fact, that it actually encodes a projection makes it even more difficult.
[20:14] <Yann> To be precise, we have lost information. Not all sides of the local objects are represented in the map, or in other words,
[20:14] <Yann> A reflection vector can map into a direction that is not sampled by out reflection map.
[20:15] <Yann> We have to live with that, and in practice, the artifact is not really noticeable, if you follow some rules on the water boundaries.
[20:15] <Yann> We need a fast and efficient algorithm to compute the distortion of the projective texture coordinates into a pre-projected texture.
[20:16] <Yann> And that's easier than you might think.
[20:16] <Yann> Imagine a plane floating slightly above the water surface.
[20:16] <Yann> Not graphically, just in a mathematical sense.
[20:17] <Yann> At each water mesh vertex, we have normal vector (as computed by finite differencing above),
[20:17] <Yann> and that normal is a measure of how much the reflection vector is deflected from the vertical direction.
[20:18] <Yann> The other parameter usually required to get the reflection vector, is the view vector. But this one is already encoded in the projective map, so we can ignore it.
[20:19] <Yann> The trick now, it to intersect the normal of each vertex with the imaginary plane above the surface, and compute the length between the vertex and the intersection point.
[20:20] <Yann> Note that we do not take the heightmap values into account here, only the normal. The water surface is assumed flat for this computation.
[20:21] <Yann> Now that we have the length, we displace the 3D texture coordinates (we feed into the projective texture matrix) along the normal by the amount of the length.
[20:21] <Yann> That sounds complicated, but it's not:
[20:21] <Yann> our length (between flat water surface, and intersected point on the virtual plane) is l.
[20:22] <Yann> Our vertex position is P (3D vector), and our (normalized) normal at that vertex is N.
[20:22] <Yann> The projective texture coordinates for that vertex are simply: T = P + l*N
[20:23] <Yann> We feed those texcoords into our texture matrix (we loaded earlier with the combined camera view and projection),
[20:23] <Yann> and proceed as before.
[20:24] <Yann> The result will be nice looking distorted reflections on the water surface, that will evolve as the waves flow.
[20:24] <Yann> There is still a question open: how far above the surface should the virtual plane float ?
[20:25] <Yann> Simple answer: as much as you like. The more distance between the water base surface, and the plane, the larger the distortions due to wave movement.
[20:25] <Yann> You have to experiment a little, until you find a good value.
[20:26] <Yann> Here is a simple screenshot showing the effect: membres.lycos.fr/ylmb/images/water1.jpg
[20:26] <Yann> (don't mind the crappy geometry, I'm no 3D artist ;).
[20:26] <Yann> OK, any questions so far ? (unmod ?)
[20:27] * RedBeard sets mode: -m
[20:27] <CoffeeMug> can you expect the fps to drastically drop whenever water enters the view frustum because essentially you have to render the scene twice?
[20:27] <noisecrime> can't get that nortmals.cpp link to download, could someone email it to me pls
[20:27] <Yann> There will be a slight drop, yes, but you have the same issue with normal opaque reflections.
[20:28] <RedBeard> you could cache the rendered texture while the scene & camera don't change
[20:28] <Yann> (it's: membres.lycos.fr/ylmb/source/normals.cpp)
[20:28] <Aasen> what about specular highlights if the wave form is severly distorted or angled?
[20:28] <Yann> Red: yes, you can (and should) do that. You'll need to update, as soon as an anmiated object moves, obviously.
[20:29] <Yann> Aaasen: coming up ;)
[20:29] <CoffeeMug> Also, can you explain why you have to use two different methods? You could use the same method for large bodies of water, only a smaller texture and you'll be able to see the landscape features reflected
[20:29] <CoffeeMug> in case there are mountains far away over the lake
[20:29] <aniero> will you be covering a method for blending between reflection and refraction based on the viewing angle?
[20:29] <noisecrime> hmm, that link just doesn't download for me ;(
[20:29] <Yann> Well, you cannot use a cubemap to reflect local geometry, as it is essentially only valid for a single point in space.
[20:30] <Yann> aniero: yes, the Fresnel effect, coming up too :)
[20:30] <aniero> excellent. =)
[20:30] <Yann> damn, this lecture is getting longer than expected ;)
[20:30] <EchoQ> no complaints here
[20:30] <aniero> i don't mind, not in the slightest. :>
[20:30] <vox> fine by me
[20:30] <Eric650> Just as a general question, and since I came in late, this might have already been answered, but I'm just curious on what you think about the general practicality/feasability of these water methods in an actual game?
[20:30] <Yann> Coffee: but you can of course combine both. The cubemap thing is better in terms of performance, as you don
[20:31] <Yann> 't have to render the scene twice.
[20:31] <Yann> Eric: depends on your overall framerate requirements and target hardware. On good hardware (GF4+), I don't see any problems.
[20:31] <Graem> that sample you showed, whats the fps it gets?
[20:31] <Yann> Around 120
[20:31] <Yann> (GF4)
[20:32] <Graem> k
[20:32] <Graem> opengl/directx?
[20:32] <Eric650> cool
[20:32] <Yann> OGL.
[20:32] <Yann> OK, ready for refractions and Fresnel ?
[20:32] <Aasen> Ready!
[20:32] <Yann> Mod please !
[20:32] <Rajansky> ready :)
[20:32] <trent> FRESNEL!
[20:32] <D33> Ready!
[20:32] * trent sets mode: +m
[20:32] <Yann> So, now we have nice reflections.
[20:33] <Yann> But look at the (imaginary) lake in front of your eyes.
[20:33] <Yann> Try to look straight down. You won't see any reflections, you'll see straight through the surface, no signs of reflections here.
[20:34] <trent> ...
[20:34] * trent looks down
[20:34] <trent> Lies, I see my feet!
[20:34] <Yann> :) Actually, the reflection on a water surface (or on any reflective object for that matter) is a function of the viewangle, lightsource position, and various other parameters.
[20:35] <Yann> If you look at the sea, you will notice that the further the water goes towards the horizon, the stronger the reflection.
[20:35] <Yann> Actually, long before your eyes reach the horizon, the water will already look like a perfect mirror.
[20:36] <Yann> That's because your viewangle is very small, compared to the surfaces position and normal.
[20:37] <Yann> The incident light will bounce of the surface at this angle, directly into your eyes. If you look down, however,
[20:37] <Yann> there is no such bounce, the refracted light from beneath the surface will reach your eyes.
[20:37] <Yann> The weighting between reflected and refracted light is called the Fresnel effect.
[20:38] <RedBeard> i've only taken basic physics, but isn't this mirror-like effect referred to as "total internal reflection"?
[20:38] <Yann> In computer graphics, it is often expressed as a function in the 0->1 range, used as an interpolation factor between reflected and refracted projective textures(we'll see those in
[20:39] <Yann> a second)
[20:39] <Yann> While in real-life, the Fresnel effect is pretty complex, we can again do various approximations, in order to be able to compute it in realtime.
[20:40] <Yann> I would suggest taking a look at the following nVidia paper about per-vertex and per-pixel Fresnel: developer.nvidia.com/view.asp?IO=fresnel_wp
[20:41] <Yann> And as already mentioned, you can compute the Fresnel per vertex or per pixel.
[20:41] <Yann> The choice primarily depends on your target hardware. Per pixel has much better quality, esp. on an optically complex material such as water.
[20:42] <Yann> Per-pixel Fresnel is also required, if doing bumpmapped reflections, since the normal can then change per pixel.
[20:42] <Yann> On the other hand, per-vertex Fresnel is much less expensive, and doesn't required pixelshade resources.
[20:42] <Yann> You choose.
[20:42] <Yann> OK, so I mentioned the refraction texture above.
[20:43] <Yann> It's true, that currently, we don't have much to interpolate: we only have a reflection...
[20:43] <Yann> From an optical point of view, the fraction is a phenomenon the happens *only* at the boundary of two materials with differing indices of refraction.
[20:44] <Yann> For example, between air and water, at the surface.
[20:44] <Yann> The physical background being a change of lightspeed in different mediums, it is mathematically expressed by Snell's law.
[20:45] <Yann> And in layman's terms: a light ray simply changes it's direction, if it hits such a boundary.
[20:45] <Yann> Now, if we took the same approach as above for the reflection, we would say: compute a view depended refracted ray using Snell's law at each vertex,
[20:45] <Yann> and use it as an index into a refracted cubemap.
[20:46] <Yann> In fact, that works, but it's not worth it.
[20:46] <Yann> The cubemap approach was only valid for (infinitely) far away geometry, but not for local objects, remember ?
[20:46] <Yann> But far away, we don't see the refraction, because of the Fresnel effect !
[20:47] <Yann> So we will restrict our refractions to near-view cases.
[20:48] <Yann> And surprise, surprise, we can use exactly the same approach as for reflections, using the virtual plane.
[20:48] <Yann> But, obviously, there are some differences:
[20:48] <Yann> * first, we don't render the reflected scene into the projective refraction texture, but the parts of the scene beneath the water surface.
[20:49] <Yann> * and second, the virtual plane used to compute the texcoord displacements, must not float above the water surface, but below it.
[20:49] <Yann> Other than that, both approaches are the same.
[20:49] <Yann> So, as a final equation for our current water we get:
[20:50] <Yann> c(final) = Refraction_texture * (1 - Fresnel) + Reflection_texture * Fresnel.
[20:50] <Yann> That's pretty much it for our basic water.
[20:51] <Yann> There is still something missing, though.
[20:51] <Yann> The specular effects.
[20:51] <Yann> Those should theoretically be included in the reflection (and often they are),
[20:52] <Yann> but unfortunately, we would need HDR (high dynamic range) reflection textures to be able to encode the full intensity spectrum needed for speculars.
[20:52] <Yann> This can be done using next-gen hardware (Radeon 9800, GeForce FX), but it will not work on GF4 and below.
[20:53] <Yann> (at least, not without major tweaking around)
[20:53] <Yann> But that's not really a problem, since we can add the specular term in a second pass on the water.
[20:53] <Yann> Or, if we have enough texture units available, we could do it directly in a single pass.
[20:54] <Yann> Basically, all we have to do, is to compute the specular lighting terms at each vertex, just as why know the good old phong or blinn model.
[20:54] <Yann> *we know
[20:55] <Yann> But per-vertex, we need a fairly well tesselated (fine) mesh, in order to get good results.
[20:55] <Yann> If that is no option, we have two choices to alleviate the problem:
[20:56] <Yann> * Either use a specular environment map (just as the cubemap above, but encoding all specular sources around the water. And indexed by the specular reflection vector).
[20:56] <Yann> * Or compute the accurate specular term per pixel.
[20:56] <Yann> Again, that's more a question about your target hardware. Perpixel specular is obviously better, but takes more performance.
[20:57] <Yann> The envmap method gives pretty nice results though, especially on infinitely far away lightsources such as the sun.
[20:58] <Yann> It might create problems with local lights, just as it did with local reflections above. Either you live with it, or you use perpixel specular.
[20:58] <RedBeard> sounds like we've got a nice water-rendering method laid out :)
[20:58] <RedBeard> shall we take a break for a few minutes?
[20:58] <Yann> Yes, what you got now, is pretty much state of the art water, as often used in non-realtime renderings.
[20:58] <Yann> Yep, why not.
[20:58] * RedBeard sets mode: -m
[20:59] <BlabberBoy> HI!
[20:59] <ProgrammerDave> hello?
[20:59] <RedBeard> one thing i'd like to ask about is when you need to render & simulate true-3D water on a small scale
[21:00] <RedBeard> like a tap/faucet pouring water into a bucket or something
[21:00] <Yann> Well, that's when you want to fully simulate a 3D water body in motion.
[21:00] <Aasen> Wow nice..What about wave froth or other effects such as wave curl...I know getting a bit deep just thought i might throw it out.
[21:00] <Yann> For example your water tap, or water pouring from a bottle into a glass.
[21:00] <RedBeard> and one other thing is how to handle stuff like islands and other objects in your NSE water simulation, so the waves lap up against the shore or something
[21:01] <aniero> also, what sorts of bump maps might we use on the surface of the water itself to add detail?
[21:01] <Yann> Aasen: you can get those, if you use the right fluid dynamic solver.
[21:01] <Yann> Red: yes, you can also add boundary conditions to the solver.
[21:01] <Yann> You simply force all pipe flows to be zero in that area.
[21:01] <RedBeard> ah, kinda like cutting holes in a terrain mesh
[21:01] <Yann> The net effect will be the water reflecting from them, as if there was a barrier.
[21:02] <Yann> Yes, right.
[21:02] <Yann> aniero: that would depends on the kind of wave model you use.
[21:02] <aniero> so theoretically, as long as you have equal volume coming in on one side and going out the other, you could simulate a portion of a river with your method
[21:02] <Yann> Normally, it's the same model, but on a smaller scale, compressed into a normal map.
[21:03] <jargon> couldnt a ground height map intersection be calculated by mapping x/z from the vertical and horizontal position on screen directly onto a perspective plane in any orientation on the screen using an inverse camera function?
[21:03] <Yann> yes, that's right. You could simulate a flowing river with that.
[21:03] <illume> could you make the water green instead. I like green water. mmmm green
[21:04] <aniero> what would you recommend for simulating underwater barriers (i.e. a rock in a river, causing a bump/hole on the surface above it)? would that be the same technique, or would you need to go to the full NSE for that?
[21:04] <ProgrammerDave> green=polluted water :)
[21:04] <Yann> Red: if you are interested in highly realistic full 3D water, I would suggest reading this paper: graphics.stanford.edu/papers/water-sg02/
[21:05] <RedBeard> ah, very nice
[21:05] <Yann> aniero: difficult with a 2.5D representation. But it could be faked by adjusting a set of (negative) external forces (-E(i,j) from above).
[21:05] <ProgrammerDave> cool
[21:05] <Yann> At the location of the rocks, obviously.
[21:05] <aniero> ah, like the rock pushing the water up from below
[21:05] <Yann> Yep.
[21:05] <ProgrammerDave> I like the glass, that's cool
[21:06] <RedBeard> aniero : possibly affected by the rate of flow coming into the column
[21:06] <jargon> yann: and use a crisscrossing sineplasma and calculating the tangents to the camera eye in order to map the angle of reflection?
[21:06] <RedBeard> uh, through, not coming into
[21:06] <Yann> Yeah, nice isn't it ? But that is a challenge to get that glass realtime ;)
[21:06] <aniero> esp. with the multiple refractions going on. :P
[21:06] <aniero> (not to mention the water)
[21:07] <illume> is there a reason why dynamic things are often not reflected in water?
[21:07] <BlabberBoy> I like the looks of that site for the realistic 3d water... probly gonna be WAY over my head, but it should be interesting :)
[21:07] <Yann> jargon: well, you *could* compute the forces at the shore manually, but it's easier to simply use the inherent functionality of the simulator for that.
[21:07] <illume> is it because they cache the reflection, which only shows the static stuff?
[21:08] <Yann> Probably. Caching is an important issue for such effects. And also the reduction of framtime used on the reflection/refraction map creation.
[21:09] <jargon> yann a 4d sineplasma could be used for muddy water?
[21:09] <jargon> er murky
[21:09] <illume> toxic wastey
[21:09] <Yann> You'll have to try that, I really don't know what kind of visual effects you would get from that ;)
[21:10] <Yann> Well, I think you've pretty much reached the end of this lecture.
[21:10] <illume> thank you very much :)
[21:10] <Aasen> Thanks Yann!
[21:10] <jargon> yay
[21:10] <Yann> Not really gone into all the details, but we would need much more time for that.
[21:10] <phantom> aye, nice one Yann :)
[21:10] <BlabberBoy> over already?? :)
[21:11] <SylvainTV> 3:07
[21:11] <SylvainTV> was cool !
[21:11] <Xero-X2> nice lecture.
[21:11] <Yann> Oh, I still have a few things you could do to improve the water model :)
[21:11] <jargon> yann i have a quick question about digital data analyzers being used with water
[21:11] <noisecrime> thx Yann good lecture
[21:11] <Graem> *claps*
[21:11] <Yann> thanks, glad you enjoyed :)
[21:11] <RedBeard> many thanks Yann, and if you feel like giving another one sometime, let us know
[21:11] <BlabberBoy> yann: you should do another sometime for the improvement and stuff...
[21:11] <BlabberBoy> maybe some underwater stuff
[21:11] <RedBeard> was a very nice presentation with lots of good information
[21:12] <trent> Nice job Yann.
[21:12] <Yann> Yeah, it was fun. Underwater stuff is very interesting: caustics, godrays, etc.
[21:12] <Yann> Next time ;)
[21:12] <jargon> cya yann
[21:12] <jargon> :D
[21:12] <jargon> thank you for the nice lecture :d
[21:13] <jargon> erm :D
[21:13] <EchoQ> thx indeed
[21:13] <Yann> You're welcome.
[21:13] <Yann> OK, any final questions before I go to bed ? It's 3 am over here :)
[21:13] <trent> Yann: give me all your source code.
[21:13] <mg|study> (Thanks for the lecture, Yann. Peace out.)
[21:13] <jargon> murky water, or smoke
[21:13] <Yann> lol :)
[21:13] <trent> Errr, that's not a question.
[21:13] <Xero-X2> lol
[21:14] <aniero> thanks yann
[21:14] <trent> Yann: Will you give me all your source code? *puts gun to Yann's head*
[21:14] <Yann> Ouch... uh, you'll have to kill me to get... oh, oops... :D
[21:14] <trent> I think there is a moth that is living in my hair.
[21:14] <BlabberBoy> trent... back in the well!
[21:14] <trent> bleh, gotta reconnect.
[21:14] <BlabberBoy> that's better :)
[21:14] <trent> damn sam.
[21:14] <Yann> hmmm, smoke, also very interesting topic...
[21:14] <Aasen> Im still absorbing the nvidia fresnel effects paper
[21:15] <Yann> 3D turbulent fluid solvers, hmm, MORE MATH, hmmm...
[21:15] <aniero> i thought by getting into computer science i'd be getting away from math
[21:15] <jargon> i was thinking of using a 4d sineplasma for flames, smoke, murky water, earth composition etc
[21:15] <aniero> NOT SO!
[21:16] <Yann> jargon, using a multidimensional plasma is very similar to the FFT method I outlined above.
Some additional information posted by chatters in another channel:
Text file last modified on April 28, 2007, 10:28:27 PM, MST.
All content Copyright © 2020 Andy Campbell.
Permission to reproduce content, in any format,
by explicit written permission of author only.
"She got her looks from her father. He's a plastic surgeon."
- Groucho Marx