[Question] How to achieve a film grain effect?

Hey all,



I’m trying to implement a film grain effect into an application based on jme. The end result should look like this.



Additionally, the effect should meet the following requirements:



  • Noise should change per frame, but can repeat itself after a second or so.

  • Should run smoothly at 25 fps, resolution 1280x820

  • Target platform is 2,5GHz Quad Core, 4GB RAM, Dual Geforce 8600



At the moment I'm unsure how to approach the solution:


  • Should I use prerendered textures and apply them to the scene I rendered to a fullscreen quad before? Wouldn't that use too much video memory?

  • Should I generate random noise (Java or glsl) and apply that? Is that even doable performance-wise, considering there might be other shader effects running at the same time?



It would be highly appreciated if some of the more experienced jME users could give me a hint on how to proceed.

Render a screen aligned quad with an additive blend alpha mode, the quad will have a 3D color noise texture applied which you can generate during loading. Every frame, increment the Z value of the texture matrix so a different part of the 3D noise is applied each frame.

A very clean and fast solution… Should work on all CPUs and video cards supporting 3D textures.

Thanks for the reply. I'm not that familiar with 3d textures, but your solution sounds good. Will investigate, and hopefully return with a working example. :slight_smile:

Hello again,



I finally managed to find the time to implement my film grain effect as a separate RenderPass. It scales to any screen size and performance is good, too. You'll find a screen shot attached to this post.



One problem remains: The intensity of the texture (the alpha value, if you want) is currently hard-coded into NoiseTexture3D and has to be set before the Noise Texture is generated. It would be nice to be able to upgrade this during runtime.



Question

How can i set the Alphavalue of a Texture during run time?



Here is the source:





NoiseTexture3D


import java.nio.ByteBuffer;
import java.util.Random;

import com.jme.image.Image;
import com.jme.image.Texture;

/**
 * <code>NoiseTexture3D</code> creates and holds a threedimensional noise Texture
 * filled with value noise.
 *
 * @author schubj
 *
 */
public class NoiseTexture3D {
   
   private Texture[] noise3D;
   private Random rng;
   private double intensity;
   
   /**
    *
    * @return The Noise intensity.
    */   
   public double getIntensity() {
      return intensity;
   }

   /**
    * Set the intensity of Noise to a value between 0.0 (invisible) to 1.0 (fully visible).
    * Default value is 0.3. You can only set the intensity before you use <code>NoiseTexture3D.generate(int width, int height)</code>
    */
   public void setIntensity(double intensity) {
      if(intensity > 1.0) {
         intensity = 1.0;
      } else if (intensity < 0.0) {
         intensity = 0.0;
      }
      this.intensity = intensity;
   }
   
   /** <code>NoiseTexture3D</code> creates and holds a threedimensional noise Texture
    * filled with value noise.
    *
    * @param depth The number of textures available. The more textures you set, the more
    * variety you get.
    */
   public NoiseTexture3D(int depth) {
      noise3D = new Texture[depth];
      this.intensity = 0.3;
      rng = new Random();
   }
   
   /**
    * Creates the threedimensional Noise Texture. Should be called after intensity is set.
    * @param width Width of the Texture. Should be a power of two (32,64,128)
    * @param height Height of the Texture. Should be a power of two (32,64,128)
    */
   public void generate(int width, int height) {
      for(int i = 0; i < noise3D.length; i++) {
         noise3D[i] = generateNoiseTexture(width, height);
      }
   }
   
   /**
    *
    * @return The texture to be used by a texture state to be displayed.
    */
   public Texture get() {
      int pos = rng.nextInt(noise3D.length);
      return noise3D[pos];
   }
   
   /**
    * Generates one Texture filled with value noise. Only for internal use.
    */
   private Texture generateNoiseTexture(int width, int height) {
      ByteBuffer data = ByteBuffer.allocate(width*height*3);
      
      for (int y = 0; y < height; y++) {
         for (int x = 0; x < width; x++) {

            byte pixel = (byte) (int) ((intensity*255) * rng.nextFloat());
            data.put(pixel);
            data.put(pixel);
            data.put(pixel);
            
         }
      }
      
      Image image = new Image(1,width,height,data);

      Texture texture = new Texture();
      texture.setFilter( Texture.FM_LINEAR );
      texture.setWrap(Texture.WM_WRAP_S_WRAP_T);
      texture.setImage(image);

      return texture;
   }
   
}



NoiseRenderPass


package viset.shaders.renderpasses;

import java.nio.FloatBuffer;

import com.jme.image.Texture;
import com.jme.renderer.Renderer;
import com.jme.renderer.TextureRenderer;
import com.jme.renderer.pass.Pass;
import com.jme.scene.batch.TriangleBatch;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;

/**
 * <code>NoiseRenderPass</code> is a pass that Renders a transparent Noise Texture over normal
 * geometry. It can be used in any Pass Game. The Texture automatically tiles to accomodate the whole screen.
 * It should be added last to the <code>PassManager</code>.
 *
 * @author schubj
 *
 */
public class NoiseRenderPass extends Pass {
   
   private NoiseTexture3D noise3d;
   private TextureState ts;
   private Quad fullScreenQuad;

   /**
    *
    * @return The Noise intensity.
    */   
   public double getIntensity() {
      return noise3d.getIntensity();
   }

   /**
    * Set the intensity of Noise to a value between 0.0 (invisible) to 1.0 (fully visible).
    * Default value is 0.3.
    */
   public void setIntensity(double intensity) {
      if(intensity > 1.0) {
         intensity = 1.0;
      } else if (intensity < 0.0) {
         intensity = 0.0;
      }
      noise3d.setIntensity(intensity);
   }

   /**
    * <code>NoiseRenderPass</code> is a pass that Renders a transparent Noise Texture over normal
    * geometry. It can be used in any Pass Game.
    *
    * It should be added last to the <code>PassManager</code>
    *
    * @author schubj
    *
    */
   public NoiseRenderPass() {
      DisplaySystem display = DisplaySystem.getDisplaySystem();
      
      //Texture State
      ts = display.getRenderer().createTextureState();
      
      //Alpha State
      AlphaState as1 = display.getRenderer().createAlphaState();
       as1.setBlendEnabled(true);
       as1.setSrcFunction(AlphaState.SB_SRC_ALPHA);
       as1.setDstFunction(AlphaState.DB_ONE);
       as1.setTestEnabled(true);
       as1.setTestFunction(AlphaState.TF_GREATER);
       as1.setEnabled(true);
      
       //Generate Texture
        noise3d = new NoiseTexture3D(64);
        noise3d.setIntensity(0.2);
        noise3d.generate(512, 512);
        ts.setTexture(noise3d.get());
       
      //Create fullscreen quad
      fullScreenQuad = new Quad("FullScreenQuad", display.getWidth(), display.getHeight());
      fullScreenQuad.setLocalTranslation(display.getWidth() / 2, display.getHeight() / 2, 0);
      fullScreenQuad.setRenderQueueMode(Renderer.QUEUE_ORTHO);
      fullScreenQuad.setTextureCombineMode(TextureState.REPLACE);
      fullScreenQuad.setLightCombineMode(LightState.OFF);
      fullScreenQuad.setRenderState(ts);
        fullScreenQuad.setRenderState(as1);

        //Tile the texture
        int tilingvalue = display.getWidth() / 512 + 1;
        FloatBuffer tBuf = fullScreenQuad.getTextureBuffer(0, 0);
        tBuf.clear();
        tBuf.put(0).put(tilingvalue);
        tBuf.put(0).put(0);
        tBuf.put(tilingvalue).put(0);
        tBuf.put(tilingvalue).put(tilingvalue);
       
       
   }
   
   /**
    * Fetch a new Noise Texture from the <code>NoiseTexture3d</code>. Texture Pool, and
    * render the Noise. Is called automatically.
    */
   protected void doRender(Renderer r) {
      ts.setTexture(noise3d.get());
      System.out.println(ts.isEnabled());
      fullScreenQuad.setRenderState(ts);
      fullScreenQuad.updateRenderState();
      r.draw(fullScreenQuad);
   }

}



TestNoise


package viset.shaders;

import viset.shaders.renderpasses.NoiseRenderPass;

import com.jme.app.SimpleGame;
import com.jme.app.SimplePassGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.light.PointLight;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.pass.RenderPass;
import com.jme.scene.Node;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Torus;
import com.jme.scene.state.TextureState;
import com.jme.util.TextureManager;
 
/**
 * Demonstrates using the <code>NoiseRenderPass</code>
 *
 * @author schubj
 */
public class TestNoise extends SimplePassGame
{
   private NoiseRenderPass noiseRenderPass;
   
   public static void main(String[] args)
   {
      TestNoise app = new TestNoise();
      app.setDialogBehaviour(SimpleGame.ALWAYS_SHOW_PROPS_DIALOG);
      app.start();
   }
   
   /**
    * The RenderPasses have to be set up here.
    *
    * @see com.jme.app.BaseSimpleGame#simpleInitGame()
    */
 
   protected void simpleInitGame()
   {   
      //Setup camera
      cam.setFrustumPerspective(55.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000);

      //Setup lights
      PointLight light = new PointLight();
      light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
      light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
      light.setLocation(new Vector3f(0, 30, 0));
      light.setEnabled(true);
      lightState.attach(light);
      
      rootNode.attachChild(createObjects());
      
      
      //Setup Renderpasses
      RenderPass rootPass = new RenderPass();
      rootPass.add(rootNode);
      pManager.add(rootPass);
      noiseRenderPass = new NoiseRenderPass();
      pManager.add(noiseRenderPass);
   }
   
   protected void simpleUpdate() {
      
   }
   /**
    * Creates a bunch of test objects.
    *
    * @return Node holding all the objects
    */
   private Node createObjects() {
      Node objects = new Node("objects");

      Torus torus = new Torus("Torus", 50, 50, 10, 20);
      torus.setLocalTranslation(new Vector3f(50, -5, 20));
      TextureState ts = display.getRenderer().createTextureState();
      Texture t0 = TextureManager.loadTexture(
            TestBloom.class.getClassLoader().getResource(
                  "jmetest/data/images/Monkey.jpg"),
            Texture.MM_LINEAR_LINEAR,
            Texture.FM_LINEAR);
      Texture t1 = TextureManager.loadTexture(
            TestBloom.class.getClassLoader().getResource(
                  "jmetest/data/texture/north.jpg"),
            Texture.MM_LINEAR_LINEAR,
            Texture.FM_LINEAR);
      t1.setEnvironmentalMapMode(Texture.EM_SPHERE);
      ts.setTexture(t0, 0);
      ts.setTexture(t1, 1);
      ts.setEnabled(true);
      torus.setRenderState(ts);
      objects.attachChild(torus);

      ts = display.getRenderer().createTextureState();
      t0 = TextureManager.loadTexture(
            TestBloom.class.getClassLoader().getResource(
                  "jmetest/data/texture/wall.jpg"),
            Texture.MM_LINEAR_LINEAR,
            Texture.FM_LINEAR);
      t0.setWrap(Texture.WM_WRAP_S_WRAP_T);
      ts.setTexture(t0);

      Box box = new Box("box1", new Vector3f(-10, -10, -10), new Vector3f(10, 10, 10));
      box.setLocalTranslation(new Vector3f(0, -7, 0));
      box.setRenderState(ts);
      objects.attachChild(box);

      box = new Box("box2", new Vector3f(-5, -5, -5), new Vector3f(5, 5, 5));
      box.setLocalTranslation(new Vector3f(15, 10, 0));
      box.setRenderState(ts);
      objects.attachChild(box);

      box = new Box("box3", new Vector3f(-5, -5, -5), new Vector3f(5, 5, 5));
      box.setLocalTranslation(new Vector3f(0, -10, 15));
      box.setRenderState(ts);
      objects.attachChild(box);

      box = new Box("box4", new Vector3f(-5, -5, -5), new Vector3f(5, 5, 5));
      box.setLocalTranslation(new Vector3f(20, 0, 0));
      box.setRenderState(ts);
      objects.attachChild(box);

      box = new Box("box5", new Vector3f(-50, -2, -50), new Vector3f(50, 2, 50));
      box.setLocalTranslation(new Vector3f(0, -15, 0));
      box.setRenderState(ts);
      box.setModelBound(new BoundingBox());
      box.updateModelBound();
      objects.attachChild(box);

      ts = display.getRenderer().createTextureState();
      t0 = TextureManager.loadTexture(
            TestBloom.class.getClassLoader().getResource(
                  "jmetest/data/texture/cloud_land.jpg"),
            Texture.MM_LINEAR_LINEAR,
            Texture.FM_LINEAR);
      t0.setWrap(Texture.WM_WRAP_S_WRAP_T);
      ts.setTexture(t0);

      box = new Box("floor", new Vector3f(-1000, -10, -1000), new Vector3f(1000, 10, 1000));
      box.setLocalTranslation(new Vector3f(0, -100, 0));
      box.setRenderState(ts);
      box.setModelBound(new BoundingBox());
      box.updateModelBound();
      objects.attachChild(box);

      return objects;
   }
}



1 Like