14
\$\begingroup\$

Recently I've begun working on a game that takes place in a procedurally generated solar system. After a bit of a learning curve (having neither worked with Scala, OpenGL 2 ES or Libgdx before), I have a basic tech demo going where you spin around a single procedurally textured planet:

enter image description here

The problem I'm running into is the performance of the texture generation. A quick overview of what I'm doing: a planet is a cube that has been deformed to a sphere. To each side, a n x n (e.g. 256 x 256) texture is applied, which are bundled in one 8n x n texture that is sent to the fragment shader. The last two spaces are not used, they're only there to make sure the width is a power of 2. The texture is currently generated on the CPU, using the updated 2012 version of the simplex noise algorithm linked to in the paper 'Simplex noise demystified'. The scene I'm using to test the algorithm contains two spheres: the planet and the background. Both use a greyscale texture consisting of six octaves of 3D simplex noise, so for example if we choose 128x128 as the texture size there are 128 x 128 x 6 x 2 x 6 = about 1.2 million calls to the noise function.

The closest you will get to the planet is about what's shown in the screenshot and since the game's target resolution is 1280x720 that means I'd prefer to use 512x512 textures. Combine that with the fact the actual textures will of course be more complicated than basic noise (There will be a day and night texture, blended in the fragment shader based on sunlight, and a specular mask. I need noise for continents, terrain color variation, clouds, city lights, etc.) and we're looking at something like 512 x 512 x 6 x 3 x 15 = 70 million noise calls for the planet alone. In the final game, there will be activities when traveling between planets, so a wait of 5 or 10 seconds, possibly 20, would be acceptable since I can calculate the texture in the background while traveling, though obviously the faster the better.

Getting back to our test scene, performance on my PC isn't too terrible, though still too slow considering the final result is going to be about 60 times worse:

128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s

This is after I moved all performance-critical code to Java, since trying to do so in Scala was a lot worse. Running this on my phone (a Samsung Galaxy S3), however, produces a more problematic result:

128x128 :  2s
256x256 :  7s
512x512 : 29s

Already far too long, and that's not even factoring in the fact that it'll be minutes instead of seconds in the final version. Clearly something needs to be done. Personally, I see a few potential avenues, though I'm not particularly keen on any of them yet:

  • Don't precalculate the textures, but let the fragment shader calculate everything. Probably not feasible, because at one point I had the background as a fullscreen quad with a pixel shader and I got about 1 fps on my phone.
  • Use the GPU to render the texture once, store it and use the stored texture from then on. Upside: might be faster than doing it on the CPU since the GPU is supposed to be faster at floating point calculations. Downside: effects that cannot (easily) be expressed as functions of simplex noise (e.g. gas planet vortices, moon craters, etc.) are a lot more difficult to code in GLSL than in Scala/Java.
  • Calculate a large amount of noise textures and ship them with the application. I'd like to avoid this if at all possible.
  • Lower the resolution. Buys me a 4x performance gain, which isn't really enough plus I lose a lot of quality.
  • Find a faster noise algorithm. If anyone has one I'm all ears, but simplex is already supposed to be faster than perlin.
  • Adopt a pixel art style, allowing for lower resolution textures and fewer noise octaves. While I originally envisioned the game in this style, I've come to prefer the realistic approach.
  • I'm doing something wrong and the performance should already be one or two orders of magnitude better. If this is the case, please let me know.

If anyone has any suggestions, tips, workarounds, or other comments regarding this problem I'd love to hear them.

In response to Layoric, here's the code I'm using:

//The function that generates the simplex noise texture
public static Texture simplex(int size) {
    byte[] data = new byte[size * size * columns * 4];
    int offset = 0;
    for (int y = 0; y < size; y++) {
        for (int s = 0; s < columns; s++) {
            for (int x = 0; x < size; x++) {
                //Scale x and y to [-1,1] range
                double tx = ((double)x / (size - 1)) * 2 - 1;
                double ty = 1 - ((double)y / (size - 1)) * 2;

                //Determine point on cube in worldspace
                double cx = 0, cy = 0, cz = 0;
                if      (s == 0) { cx =   1; cy =  tx; cz =  ty; }
                else if (s == 1) { cx = -tx; cy =   1; cz =  ty; }
                else if (s == 2) { cx = - 1; cy = -tx; cz =  ty; }
                else if (s == 3) { cx =  tx; cy = - 1; cz =  ty; }
                else if (s == 4) { cx = -ty; cy =  tx; cz =   1; }
                else if (s == 5) { cx =  ty; cy =  tx; cz = - 1; }

                //Determine point on sphere in worldspace
                double sx = cx * Math.sqrt(1 - cy*cy/2 - cz*cz/2 + cy*cy*cz*cz/3);
                double sy = cy * Math.sqrt(1 - cz*cz/2 - cx*cx/2 + cz*cz*cx*cx/3);
                double sz = cz * Math.sqrt(1 - cx*cx/2 - cy*cy/2 + cx*cx*cy*cy/3);

                //Generate 6 octaves of noise
                float gray = (float)(SimplexNoise.fbm(6, sx, sy, sz, 8) / 2 + 0.5);

                //Set components of the current pixel
                data[offset    ] = (byte)(gray * 255);
                data[offset + 1] = (byte)(gray * 255);
                data[offset + 2] = (byte)(gray * 255);
                data[offset + 3] = (byte)(255);

                //Move to the next pixel
                offset += 4;
            }
        }
    }

    Pixmap pixmap = new Pixmap(columns * size, size, Pixmap.Format.RGBA8888);
    pixmap.getPixels().put(data).position(0);

    Texture texture = new Texture(pixmap, true);
    texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
    return texture;
}

//SimplexNoise.fbm
//The noise function is the same one found in http://webstaff.itn.liu.se/~stegu/simplexnoise/SimplexNoise.java
//the only modification being that I replaced the 32 in the last line with 16 in order to end up with
//noise in the range [-0.5, 0.5] instead of [-1,1]
public static double fbm(int octaves, double x, double y, double z, double frequency) {
    double value = 0;
    double f = frequency;
    double amp = 1;
    for (int i = 0; i < octaves; i++) {
        value += noise(x*f, y*f, z*f) * amp;
        f *= 2;
        amp /= 2;
    }
    return value; 
}
\$\endgroup\$
3
  • \$\begingroup\$ Could you post up what you currently have in Java for your noise function? Not saying there exists any performance gains to be had from it, but some one might spot something to give you a boost. \$\endgroup\$ Commented Nov 19, 2012 at 3:44
  • \$\begingroup\$ I've added the code I'm using to the original post. \$\endgroup\$
    – FalconNL
    Commented Nov 19, 2012 at 15:51
  • 1
    \$\begingroup\$ Not related to your Q per se, but you should call dispose() on your pixmap after you're done with it. \$\endgroup\$
    – junkdog
    Commented Nov 22, 2012 at 17:54

2 Answers 2

10
\$\begingroup\$

You could combine approaches (2) and (3) like this:

  • First, use GPU to generate a number of noise textures and save them. This wll be your "noise cache"; you can only do it once on the first run.
  • To generate a texture in-game, combine a few textures from cache - this should be real fast. Then, if needed, add special effects like vortices on top of that.
  • Alternatively, you can pre-generate some "special-effects" textures too, and just blend them to get final result.
\$\endgroup\$
1
  • \$\begingroup\$ +1 I think generating a bunch of textures and packaging them with the game to combine or apply simple affects to would be the best way to do it. \$\endgroup\$ Commented Nov 2, 2013 at 20:48
2
\$\begingroup\$

Procedural texture generation is a b** of a mofo in terms of compute time. It is what it is.

The best implementation of Simplex Noise I have found is Stefan Gustavson's.

Beyond improvement of actual compute time (it is actually pretty hard to get past the fact that you simply are asking for a lot out of your computer when you compute 1024x1024 procedural textures), one of the best ways to reduce perceived wait time is to have your app do as much background thread work as possible.

So start generating textures at game launch on the background thread, while the user is still fiddling with options and the menu or watching the level start trailer.

The other thing to consider is, just cache several hundred generated textures to disk and randomly select one of these at load time. More disk, but less load time.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .