Fur effect using shell rendering

Is it possible to implement this XNA shell rendering example using jMe?

http://www.ziggyware.com/readarticle.php?article_id=194



I have not written any shader code yet and I want know am I trying something that is too hard or impossible. Any help is also welcome.







should be possible i think

Definitely possible. The shader to do this is quite simple and the geometry generation can be ported easily.

Thanks for the comments. I will try to implement the fur effect. I will post an example if get it done.



Here is also the same kind of tutorial.

http://www.xbdev.net/directx3dx/specialX/Fur/index.php



I made a simple fur test code. I didn't use shaders. I just created several SharedNodes that are scaled a bit larger than the original node. Each layer has its own generated texture with alpha.



See code and the screen capture. You can freely use the code and it should work quite well with all convex shapes. Concave shapes like torus needs better scaling method than the simple local scale.



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

import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Image;
import com.jme.image.Texture;
import com.jme.image.Texture2D;
import com.jme.input.FirstPersonHandler;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.SharedNode;
import com.jme.scene.shape.Capsule;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.CullState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.util.geom.BufferUtils;


/**
 * Fur effect using shell shading.
 *
 * @author mazander
 */
public class FurTest extends SimpleGame {
    
    private Quaternion rotQuat = new Quaternion();
    private float angle = 0;
    private Vector3f axis = new Vector3f(1, 1, 0);
    private Node furryNode;


    public static void main(String[] args) {
       FurTest app = new FurTest();
        app.setConfigShowMode(ConfigShowMode.AlwaysShow);
        app.start();
    }

    protected void simpleUpdate() {
        if (tpf < 1) {
            angle = angle + (tpf * 25);
            if (angle > 360) {
                angle = 0;
            }
        }

        rotQuat.fromAngleNormalAxis(angle * FastMath.DEG_TO_RAD, axis);
        furryNode.setLocalRotation(rotQuat);
    }

    protected void simpleInitGame() {
        display.setTitle("Fur Test");
        Node node = new Node("scene");
        Capsule torus = new Capsule("", 4, 12, 12, 4, 6);
        torus.setModelBound(new BoundingBox());
        torus.updateModelBound();
        node.attachChild(torus);
        FurFactory furFactory = new FurFactory();
        furFactory.width = 256;
        furFactory.height = 256;
        furFactory.layers = 40;
        
        furryNode = furFactory.createFurryNode(node);
        rootNode.attachChild(furryNode);
        input = new FirstPersonHandler(cam);
    }

   
   public class FurFactory {
      
      public int layers = 20;
      
      public int width = 128;
      
      public int height = 128;
      
      public float hairdensity = 0.85f;
      
      public float hairLength = 0.5f;
      
      public ColorRGBA transparent = new ColorRGBA(0f,0f,0f,0f);
      
      public ColorRGBA skin = new ColorRGBA(.1f, .3f, .5f,1f);
      
      public ColorRGBA start = new ColorRGBA(.1f, .2f, .3f,1f);
      
      public ColorRGBA end = new ColorRGBA(.2f, .4f, .8f,1f);

      public Texture2D[] createTextures() {
         int totalPixels = width * height;
         Texture2D[] textures = new Texture2D[layers];
         ByteBuffer[] buffers = new ByteBuffer[layers];
         for (int i = 0; i < layers; i++) {
            buffers[i] = BufferUtils.createByteBuffer(4 * totalPixels);
         }
         
         ColorRGBA color = new ColorRGBA();
         Random random = new Random();
         int skinColor = skin.asIntARGB();
         int transparentColor = transparent.asIntARGB();
         for (int i = 0; i < totalPixels; i++) {
            float hairColor = random.nextFloat();
            int hairLength = random.nextInt(layers);
            for (int j = 0; j < layers; j++) {
               if(j == 0) {
                  buffers[j].putInt( skinColor );
               } else if (hairColor <= hairdensity) {
                  float hairThickness = j < hairLength ? (float) hairLength / j : 0f;
                  color.interpolate(start, end, hairColor / hairdensity);
                  color.a = hairThickness;
                  buffers[j].putInt(color.asIntARGB());
               } else {
                  buffers[j].putInt( transparentColor);
               }
            }
         }
         for (int i = 0; i < layers; i++) {
            Image image = new Image(Image.Format.RGBA8, width, height, buffers[i]);
            textures[i] = new Texture2D();
            textures[i].setImage(image);
            textures[i].setWrap(Texture.WrapMode.Repeat);
         }

         return textures;
      }
      
      public Node createFurryNode(Node node) {
         Texture2D[] textures = createTextures();
         return createFurryNode(node, textures);
      }
      
      public Node createFurryNode(Node node, Texture2D[] textures) {
         Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
         
         CullState cs = renderer.createCullState();
         cs.setCullFace(CullState.Face.Back);
         
         BlendState as = renderer.createBlendState();
         as.setSourceFunction(BlendState.SourceFunction.SourceAlpha );
         as.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
         as.setTestFunction(BlendState.TestFunction.Always);
         as.setBlendEnabled(true);
         as.setTestEnabled(true);
         as.setEnabled(true);

           ZBufferState zs = renderer.createZBufferState();
           zs.setEnabled(true);
           zs.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);

         Node skinNode = new Node();
         Node hairNode = new Node();
         for (int i = 0; i < layers; i++) {
            TextureState ts = renderer.createTextureState();
            ts.setTexture(textures[i]);
            // set render states
            SharedNode shared = new SharedNode(node);
            shared.setRenderState(cs);
            shared.setRenderState(ts);
            shared.setRenderState(as);
            shared.setRenderState(zs);
            // set local scale
            float scale = 1f + i * hairLength / (layers - 1);
            shared.setLocalScale(scale);
            if(i == 0) {
               skinNode.attachChild(shared);
            } else {
               hairNode.attachChild(shared);
            }
         }
         hairNode.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
           Node resultNode = new Node();
           resultNode.attachChild(skinNode);
           resultNode.attachChild(hairNode);
         
         return resultNode;
      }
   }

}

cool, the result looks pretty good, but it seems pretty memory intensive.

That is true, each layer has its own texture, because hair transparencies change from start the end. The geometry should be shared. Without hair transparency the effect looks like in the XNA example. To lower memory need you can decrease the layer count and texture sizes. I'm not familiar with shaders but I bet this is faster than multi pass shader version.

It is so cool! Thx for providing that…(and can't wait to see you monkey in action :smiley: )

mazander said:

I'm not familiar with shaders but I bet this is faster than multi pass shader version.


I'm not familiar with shaders either, but I bet you bet wrong  :| (at least on newer cards)

Very good looking results though.  ;)

I uploaded to Youtube a short video of the example that I posted to this topic. Thanks for the comments.



http://www.youtube.com/watch?v=XTHyajWCAVk

nice work, it really looks great!