Here's an improved version. It pre-calculated the normals in the controller based on a formula that i pulled out of my nose. With that change, performance is like this on my machine (Core2 Quad @3.2Ghz, AMD Radeon 5870):
1600 polygons/precalc normals: 4500fps
1600 polygons/real normals: 2500fps
10000 polygons/precalc normals: 2300fps
10000 polygons/real normals: 30fps
...so precalc normals should be fast enough IMHO.
Here's the code. You can now apply an additional scale that changes the waves' height as well as a damping factor that makes the waves narrow or wide.
import com.threed.jpct.Config;
import com.threed.jpct.FrameBuffer;
import com.threed.jpct.GenericVertexController;
import com.threed.jpct.IRenderer;
import com.threed.jpct.Matrix;
import com.threed.jpct.Object3D;
import com.threed.jpct.Primitives;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureInfo;
import com.threed.jpct.TextureManager;
import com.threed.jpct.World;
import com.threed.jpct.util.Light;
public class CheapWater {
private World world;
private FrameBuffer buffer;
private Object3D water;
private WaterController wc = null;
public static void main(String[] args) throws Exception {
new CheapWater().loop();
}
public CheapWater() throws Exception {
Config.glForceEnvMapToSecondStage = true;
Config.glUseVBO = true;
Config.glDynamicBatchSize=4000;
world = new World();
world.setAmbientLight(100, 100, 100);
TextureManager tm = TextureManager.getInstance();
tm.addTexture("water", new Texture("water3.jpg"));
tm.addTexture("envmap", new Texture("environment2.jpg"));
water = Primitives.getPlane(40, 1.5f);
TextureInfo ti = new TextureInfo(tm.getTextureID("water"));
ti.add(tm.getTextureID("envmap"), TextureInfo.MODE_MODULATE);
water.setTexture(ti);
water.setEnvmapped(Object3D.ENVMAP_ENABLED);
water.rotateX((float) Math.PI / 2f);
water.rotateMesh();
water.clearRotation();
water.build();
water.compile(true);
water.setTransparency(2);
wc = new WaterController(water, 5, 10, false);
water.getMesh().setVertexController(wc, false);
world.addObject(water);
world.getCamera().setPosition(0, -50, -50);
world.getCamera().lookAt(water.getTransformedCenter());
Light light = new Light(world);
light.setAttenuation(-1);
light.setIntensity(255, 255, 255);
light.setPosition(new SimpleVector(100, -50, -20));
}
private void loop() throws Exception {
buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_GL_AA_2X);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
buffer.enableRenderer(IRenderer.RENDERER_OPENGL);
long s = System.nanoTime() / 1000000L;
long ss = System.currentTimeMillis();
int fps = 0;
while (!org.lwjgl.opengl.Display.isCloseRequested()) {
buffer.clear(java.awt.Color.BLUE);
long ticks = ((System.nanoTime() / 1000000L) - s) / 10;
if (ticks > 0) {
s = System.nanoTime() / 1000000L;
wc.update(0.5f * (float) ticks);
water.getMesh().applyVertexController();
}
world.renderScene(buffer);
world.draw(buffer);
buffer.update();
buffer.displayGLOnly();
fps++;
if (System.currentTimeMillis() - ss >= 1000) {
System.out.println(fps + "fps");
fps = 0;
ss = System.currentTimeMillis();
}
}
buffer.disableRenderer(IRenderer.RENDERER_OPENGL);
buffer.dispose();
System.exit(0);
}
private static class WaterController extends GenericVertexController {
private float scale = 0;
private float damping = 0;
private SimpleVector[] preCalcNormals = null;
private SimpleVector[] preCalcNormalsNeg = null;
private float[] lastHeight = null;
private static final long serialVersionUID = 1L;
private float degreeAdd = 0;
private Object3D water = null;
private float lastUpdate = 0;
private boolean realNormals = false;
public WaterController(Object3D water, float scale, float damping, boolean realNormals) {
this.scale = scale;
this.water = water;
this.realNormals = realNormals;
this.damping = damping;
water.setTextureMatrix(new Matrix());
}
/**
* This calculates some normals...these are rather fake and in no way
* comparable to real surface normals. But they should do the trick...
*/
public boolean setup() {
SimpleVector ax = new SimpleVector(-1, 0, 1).normalize();
preCalcNormals = new SimpleVector[(int) (100f * scale)];
preCalcNormalsNeg = new SimpleVector[(int) (100f * scale)];
int end = preCalcNormals.length;
for (int i = 0; i < end; i++) {
float height = -1f + (((float) i) / (end / 2f));
SimpleVector n = new SimpleVector(0, -1, 0);
SimpleVector n2 = new SimpleVector(0, -1, 0);
Matrix m = new Matrix();
Matrix m2 = new Matrix();
if (height <= 0) {
float val = (float) Math.sqrt((height + 1) * (Math.PI / 7f));
m.rotateAxis(ax, val);
m2.rotateAxis(ax, -val);
} else {
float val = (float) Math.sqrt((1 - height) * (Math.PI / 7f));
m.rotateAxis(ax, val);
m2.rotateAxis(ax, -val);
}
n.rotate(m);
n2.rotate(m);
preCalcNormals[i] = n;
preCalcNormalsNeg[i] = n2;
}
SimpleVector[] source = this.getSourceMesh();
lastHeight = new float[source.length];
for (int i = 0; i < source.length; i++) {
lastHeight[i] = 0;
}
return true;
}
public void update(float inc) {
degreeAdd += inc;
lastUpdate = inc;
}
public void apply() {
SimpleVector[] source = this.getSourceMesh();
SimpleVector[] dest = this.getDestinationMesh();
SimpleVector[] destNormals = this.getDestinationNormals();
int end = source.length;
int nEnd = preCalcNormals.length;
for (int i = 0; i < end; i++) {
SimpleVector s = source[i];
SimpleVector d = dest[i];
float sin = (float) Math.sin((degreeAdd + s.x + s.z) / damping);
d.set(s.x, s.y + sin * scale, s.z);
int iHeight = (int) ((sin + 1) * (nEnd / 2));
if (lastHeight[i] > sin) {
destNormals[i].set(preCalcNormalsNeg[iHeight]);
} else {
destNormals[i].set(preCalcNormals[iHeight]);
}
lastHeight[i] = sin;
}
water.touch();
if (realNormals) {
water.calcNormals();
}
float tr = lastUpdate / 333f;
water.getTextureMatrix().translate(tr, -tr, 0);
}
}
}