Outline render pass

hi there



here's yet another Pass. it renders the outline of objects in the scene. the code is part of the cell shading technique as described at gamedev.net: http://www.gamedev.net/reference/articles/article1438.asp

the same technique can be used to render wireframe on top of models (wireframe and models visible).

if you people think this stuff is useful, please tell me, then i'll be glad do donate it to jme. othewise it might land in my own game (which is GPL not BDS).

any improvements, suggestions and opinions are welcome.



a screenshot is available at : http://moenia.sourceforge.net/images/screenshots/outline.png

(note that it was made without antialiasing or line smoothing.



TestOutlineRenderPass.java


import com.jme.app.SimplePassGame;
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.SharedNode;
import com.jmex.model.XMLparser.JmeBinaryReader;

/*
 * Created on Jan 19, 2006
 */

/**
 * @author Beskid Lucian Cristian
 */
public class TestOutlineRenderPass extends SimplePassGame
{
  private Node model = null;

  protected void simpleInitGame()
  {
    display.setTitle("Outline render pass test");
    display.getRenderer().setBackgroundColor(new ColorRGBA(0.5f, 0.7f, 1f, 1f));
   
    cam.setFrustumPerspective(55.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000);
    cam.setLocation(new Vector3f(500, 0, 0));
    cam.lookAt(new Vector3f(0,0,0), new Vector3f(0,1,0));
   
    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);
   
    Node normalObjects = new Node("normal");
    Node outlinedObjects = new Node("outlined");
   
    OutlineRenderPass outlineRenderPass = new OutlineRenderPass();
    outlineRenderPass.add(outlinedObjects);
    outlineRenderPass.setEnabled(true);
   
    RenderPass renderPass = new RenderPass();
    renderPass.add(normalObjects);
    renderPass.setEnabled(true);
   
    pManager.add(outlineRenderPass);
    pManager.add(renderPass);
   
    rootNode.attachChild(normalObjects);
    rootNode.attachChild(outlinedObjects);
   
    // load objects
    JmeBinaryReader reader = new JmeBinaryReader();
   
    try
    {
      model = reader.loadBinaryFormat(getClass().getClassLoader().getResourceAsStream("jmetest/data/model/bike.jme"));
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }

    if (model != null) {
      SharedNode outlinedModel = new SharedNode("outlined.model", model);
      SharedNode normalModel = new SharedNode("normal.model", model);
     
      outlinedModel.setLocalTranslation(new Vector3f(0, 200, 0));
      normalModel.setLocalTranslation(new Vector3f(0, -200, 0));
     
      outlinedObjects.attachChild(outlinedModel);
      normalObjects.attachChild(normalModel);
    }
    rootNode.updateGeometricState(0, true);
    rootNode.updateRenderState();
  }

  public static void main(String[] args)
  {
    TestOutlineRenderPass app = new TestOutlineRenderPass();
    app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
    app.start();
  }

}



OutlineRenderPass.java


import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.renderer.pass.Pass;
import com.jme.renderer.pass.RenderPass;
import com.jme.scene.Spatial;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.CullState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.RenderState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.WireframeState;
import com.jme.system.DisplaySystem;

/**
 * @author Beskid Lucian Cristian
 */
public class OutlineRenderPass extends RenderPass
{
  public static final float DEFAULT_LINE_WIDTH = 3f;
  public static final ColorRGBA DEFAULT_OUTLINE_COLOR = ColorRGBA.darkGray;
  public static final Pass DEFAULT_SECOND_RENDER_PASS = new RenderPass();
 
  private RenderState[] backupStates = new RenderState[RenderState.RS_MAX_STATE];
  // render states needed to draw the outline
  private WireframeState wireframeState;
  private CullState cullFront;
  private LightState noLights;
  private TextureState noTexture;
  private AlphaState alphaState;
 
  public OutlineRenderPass()
  {
    wireframeState = DisplaySystem.getDisplaySystem().getRenderer().createWireframeState();
    wireframeState.setFace(WireframeState.WS_FRONT);
    wireframeState.setLineWidth(DEFAULT_LINE_WIDTH);
    wireframeState.setEnabled(true);
   
    cullFront = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
    cullFront.setCullMode(CullState.CS_FRONT);
    cullFront.setEnabled(true);
   
    noLights = DisplaySystem.getDisplaySystem().getRenderer().createLightState();
    noLights.setGlobalAmbient(DEFAULT_OUTLINE_COLOR);
    noLights.setEnabled(true);
   
    noTexture = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
    noTexture.setEnabled(true);
   
    alphaState = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
    alphaState.setSrcFunction(AlphaState.SB_SRC_ALPHA);
    alphaState.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
    alphaState.setBlendEnabled(true);
    alphaState.setEnabled(true);
   
  }

  public void doRender(Renderer renderer)
  {
    // if there's nothing to do
    if (spatials.size() == 0) return;
   
    // normal render
    super.doRender(renderer);
   
    // set up the render states
    backupRenderStates();
    Spatial.enforceState(wireframeState);
    Spatial.enforceState(cullFront);
    Spatial.enforceState(noLights);
    Spatial.enforceState(noTexture);
    Spatial.enforceState(alphaState);
   
    // this will draw the wireframe
    for (int i = 0; i < spatials.size(); ++i) {
      Spatial spatial = (Spatial)spatials.get(i);
      spatial.onDraw(renderer);
     
    }
    // restore the render states
    restoreRenderStates();
  }
 
  private void backupRenderStates() {
    for (int i = 0; i < RenderState.RS_MAX_STATE; ++i) {
      backupStates[i] = Spatial.enforcedStateList[i];
    }
  }
 
  private void restoreRenderStates() {
    for (int i = 0; i < RenderState.RS_MAX_STATE; ++i) {
      Spatial.enforcedStateList[i] = backupStates[i];
    }
  }
 
  public void setOutlineWidth(float width) {
    wireframeState.setLineWidth(width);
  }
 
  public float getOutlineWidth() {
    return wireframeState.getLineWidth();
  }

  public void setOutlineColor(ColorRGBA outlineColor) {
    noLights.setGlobalAmbient(outlineColor);
  }
 
  public ColorRGBA getOutlineColor() {
    return noLights.getGlobalAmbient();
  }
}


Looks good. And a good example of how to use renderpasses. Let's get it in once you add antialiasing/smoothing for the lines :slight_smile:

Great, Sfera. I'm playing with it right now. Very nice results.

thank you for your feedback. i added that line of code to OutlineRenderPass.

 wireframeState.setAntialiased(true);





here are the screenshots for comparision. i can't decide if the outline looks better or not whith smoothed lines enabled so here are some screenshots for comparision:



no antialiasing:

http://moenia.sourceforge.net/images/screenshots/outline.png

http://moenia.sourceforge.net/images/screenshots/smoothed.outline.png



2x antialiasing enabled:

http://moenia.sourceforge.net/images/screenshots/2xAA.unsmoothed.outline.png

http://moenia.sourceforge.net/images/screenshots/2xAA.smoothed.outline.png



the comparision might not be objective because the camera angle is a bit different for each screenshot.

well, i'll let you decide what looks best.

Now try smoothed with AlphaState.DB_ONE instead of AlphaState.DB_ONE_MINUS_SRC_ALPHA :smiley:

with a black outline, that would look like this:

http://moenia.sourceforge.net/images/screenshots/outline.db_one.png  :expressionless:

I loaded the MD2 (Dr. Freak) file and got a bit unexpected results. I was basically looking at the wire frame instead of the outline. Any ideas?

the md2 model has front face winding of it's polys perhaps? i guess the outline pass should flip the cullingstates instead of setting them hard to front/back

you are right mrcoder. dr evil is evil enough to have its polys defined in the opposite direction. if i change the cull mode it's displayed as it should be.



the problem is that if i want to set the right cull mode to all the models, i have to walk the entire graph, searching for geometry, creating temporary states for each geometry, and afterards i have to restore everything. i don't know if it's not a litte bit too much work for a RenderPass to touch every geometry in the graph.

maybe there is another option: make the render pass configurable so the user can set the cull mode. he will know what models he uses and set the right culling mode. if he uses both kind of models, he will have to place them in separate render passes, each configured properly. this alternative would also enable setting the cull state in such a manner that the wireframe is displayed on top of the model (sell the bug as a feature) :wink:

would be good to find the optimal solution to that kind of state flipping…i need exactly the same thing for rendering an inverted scene to a texture for reflections in water, i need to flip the cullmode of every object…

Something along the lines of



enforceStateFlipping(RS_CULL);



or



CullState.flipAll(boolean);



perhaps?

We could also simply expose a method that allowed setting of glFrontFace.

llama:

i implemented a CullState.setFlipCullMode(boolean) (and adjusted LWJGLCullState) but it didnt work for me. that's because i render the graph twice (normal and wireframe) and when it comes to the second pass and i enable the flip mode, the states aren't applied again because they didn't change (only the static attribute of CullState changes). maybe i overlooked/misunderstood something. any hints?  :?

Try calling Spatial.clearCurrentStates()



Also make sure you call renderer.renderQueue() at the end of your pass so objects in the renderQueue get drawn using the states you enforce. (It currently works because your example doesn't place any objects in there).



Another small thing… why do you extend RenderPass instead of Pass? :slight_smile:

llama said:

Another small thing... why do you extend RenderPass instead of Pass? :)


so i can call super.doRender()  :P
i was too lazy to write that loop again  :wink:

btw: i'll try that state clearing.

OK… I actually didn't see that one :roll:



Uhm… good reason :slight_smile:

now it works. just don't forget to set the proper cull state. still need testing. that CullState static method should make mrcoder happy too :slight_smile:



thanks for the hint llama! ( niiice llama ! pat-pat-pat )  :smiley:



changes in CullState




    /** Tells wether to flip the cull states or not **/
    protected static boolean flipCulling = false;

    /**
     * Provide a hint that all cull states should be flipped.
     * In other words if the cull mode is set to CS_FRONT, then
     * CS_BACK will be used. For CS_BACK the opposite will happen.
     * If the cull mode is set to CS_NONE this hint won't have any effect.
     * @param flip true if the all cull states should be flipped.
     */
    public static void setFlipCullMode(boolean flip) {
      flipCulling = flip;
    }

    /**
     * @return true if all cull states should be are flipped, false otherwise.
     */
    public boolean isFlipCullMode() {
      return flipCulling;
    }



changes in LWJGLCullSate


    public void apply()
    {
      if (isEnabled()) {
        if (!flipCulling) {
          switch (cullMode) {
            case CS_FRONT:
              GL11.glCullFace(GL11.GL_FRONT);
              GL11.glEnable(GL11.GL_CULL_FACE);
              return;
            case CS_BACK:
              GL11.glCullFace(GL11.GL_BACK);
              GL11.glEnable(GL11.GL_CULL_FACE);
              return;
          }
        }
        else {
          switch (cullMode) {
            case CS_FRONT:
              GL11.glCullFace(GL11.GL_BACK);
              GL11.glEnable(GL11.GL_CULL_FACE);
              return;
            case CS_BACK:
              GL11.glCullFace(GL11.GL_FRONT);
              GL11.glEnable(GL11.GL_CULL_FACE);
              return;
          }
        }
      }
      GL11.glDisable(GL11.GL_CULL_FACE);
    }



OutlineRenderPass



import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.renderer.pass.Pass;
import com.jme.renderer.pass.RenderPass;
import com.jme.scene.Spatial;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.CullState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.RenderState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.WireframeState;
import com.jme.system.DisplaySystem;

/**
 * @author Lucian Cristian Beskid
 */
public class OutlineRenderPass extends RenderPass
{
  public static final float DEFAULT_LINE_WIDTH = 3f;
  public static final ColorRGBA DEFAULT_OUTLINE_COLOR = ColorRGBA.black;

  // render states used to back up the original ones
  private RenderState wireframeStateBackup;
  private RenderState lightStateBackup;
  private RenderState textureStateBackup;
  private RenderState alphaStateBackup;

  // render states needed to draw the outline
  private WireframeState wireframeState;
  private LightState noLights;
  private TextureState noTexture;
  private AlphaState alphaState;
 
  public OutlineRenderPass()
  {
    wireframeState = DisplaySystem.getDisplaySystem().getRenderer().createWireframeState();
    wireframeState.setFace(WireframeState.WS_FRONT);
    wireframeState.setLineWidth(DEFAULT_LINE_WIDTH);
    wireframeState.setAntialiased(true);
    wireframeState.setEnabled(true);
   
    noLights = DisplaySystem.getDisplaySystem().getRenderer().createLightState();
    noLights.setGlobalAmbient(DEFAULT_OUTLINE_COLOR);
    noLights.setEnabled(true);
   
    noTexture = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
    noTexture.setEnabled(true);
   
    alphaState = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
    alphaState.setSrcFunction(AlphaState.SB_SRC_ALPHA);
    alphaState.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
    alphaState.setBlendEnabled(true);
    alphaState.setEnabled(true);
   
  }

  public void doRender(Renderer renderer)
  {
    // if there's nothing to do
    if (spatials.size() == 0) return;
   
    // normal render
    super.doRender(renderer);
   
    // set up the render states
    backupRenderStates();
    CullState.setFlipCullMode(true);
    Spatial.clearCurrentStates();
    Spatial.enforceState(wireframeState);
    Spatial.enforceState(noLights);
    Spatial.enforceState(noTexture);
    Spatial.enforceState(alphaState);
   
    // this will draw the wireframe
    for (int i = 0; i < spatials.size(); ++i) {
      Spatial spatial = (Spatial)spatials.get(i);
      spatial.onDraw(renderer);
     
    }
    renderer.renderQueue();
   
    // restore the render states
    restoreRenderStates();
    CullState.setFlipCullMode(false);
   
  }
 
  private void backupRenderStates() {
    wireframeStateBackup = Spatial.enforcedStateList[RenderState.RS_WIREFRAME];
    lightStateBackup = Spatial.enforcedStateList[RenderState.RS_LIGHT];
    textureStateBackup = Spatial.enforcedStateList[RenderState.RS_TEXTURE];
    alphaStateBackup = Spatial.enforcedStateList[RenderState.RS_ALPHA];
  }
 
  private void restoreRenderStates() {
    Spatial.enforcedStateList[RenderState.RS_WIREFRAME] = wireframeStateBackup;
    Spatial.enforcedStateList[RenderState.RS_LIGHT] = lightStateBackup;
    Spatial.enforcedStateList[RenderState.RS_TEXTURE] = textureStateBackup;
    Spatial.enforcedStateList[RenderState.RS_ALPHA] = alphaStateBackup;
  }
 
  public void setOutlineWidth(float width) {
    wireframeState.setLineWidth(width);
  }
 
  public float getOutlineWidth() {
    return wireframeState.getLineWidth();
  }

  public void setOutlineColor(ColorRGBA outlineColor) {
    noLights.setGlobalAmbient(outlineColor);
  }
 
  public ColorRGBA getOutlineColor() {
    return noLights.getGlobalAmbient();
  }
}



TestOutlineRenderPass


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import OutlineRenderPass;

import com.jme.app.SimplePassGame;
import com.jme.light.PointLight;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.SharedNode;
import com.jme.scene.state.CullState;
import com.jmex.model.XMLparser.JmeBinaryReader;
import com.jmex.model.XMLparser.Converters.Md2ToJme;

/**
 * @author Beskid Lucian Cristian
 */
public class TestOutlineRenderPass extends SimplePassGame
{
  private Node model = null;

  protected void simpleInitGame()
  {
    display.setTitle("Outline render pass test");
    display.getRenderer().setBackgroundColor(new ColorRGBA(0.5f, 0.7f, 1f, 1f));
   
    cam.setFrustumPerspective(55.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000);
    cam.setLocation(new Vector3f(50, 0, 0));
    cam.lookAt(new Vector3f(0,0,0), new Vector3f(0,1,0));
   
    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);
   
    Node outlinedObjects = new Node("outlined");
   
    OutlineRenderPass outlineRenderPass = new OutlineRenderPass();
    outlineRenderPass.add(outlinedObjects);
    outlineRenderPass.setEnabled(true);
   
    pManager.add(outlineRenderPass);
   
    rootNode.attachChild(outlinedObjects);
   
    // load objects
    JmeBinaryReader reader = new JmeBinaryReader();
   
    try
    {
      // load/convert the model
      Md2ToJme converter = new Md2ToJme();
      ByteArrayOutputStream stream = new ByteArrayOutputStream();
      converter.convert(
        getClass().getClassLoader().getResourceAsStream("jmetest/data/model/drfreak.md2"),
        stream);
      model = reader.loadBinaryFormat(new ByteArrayInputStream(stream.toByteArray()));
     
      // don't forget to set the proper cull state otherwise you might get weird effects
      CullState cullState = display.getRenderer().createCullState();
      cullState.setCullMode(CullState.CS_FRONT);
      cullState.setEnabled(true);
      model.setRenderState(cullState);
     
     
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }

    if (model != null) {
      SharedNode outlinedModel = new SharedNode("outlined.model", model);
     
      outlinedObjects.attachChild(outlinedModel);
    }
    rootNode.updateGeometricState(0, true);
    rootNode.updateRenderState();
  }

  public static void main(String[] args)
  {
    TestOutlineRenderPass app = new TestOutlineRenderPass();
    app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
    app.start();
  }
}



EDIT: had to edit a few times to correct some mistakes :)

Ok, I created an issue: https://jme.dev.java.net/issues/show_bug.cgi?id=183



And I'll check it in today(ish)

thanks a lot

Checked in. Redid the changes to CullingState a bit (already had something local here), added a getter/setter for the AlphaState, and a sprinkle of JavaDoc here and there.