How to share one shader between several objects with extra attribute pointers?

This is a tricky one and I have not been able to figure out, yet.



Let’s say I have two TriMesh instances “foo” and “bar”. Both were loaded from an OBJ file and have their tangent and bitangent information calculated using the com.jme.util.geom.TangentBinormalGenerator. Now I have two extra FloatBuffers per object which never find their way into the shader as they’re not associated with an attribute in the vertex shader.



I can use fooShaderState.setAttributePointer(“Tangent”, 3, true, 0, foo.getTangentBuffer()) and fooShaderState.setAttributePointer(“Binormal”, 3, true, 0, foo.getBinormalBuffer()), but that essentially requires me to duplicate the shader for those two objects. Since I’m rendering several objects with the same shader I find it wasteful to create that many GLSLShaderObjectsState instances with the same compiled program.



Did I miss something? Is it maybe not that wasteful as the attribute pointers don’t have to be reset on every rendering of the object? I’ve read somewhere else in the forum that setting an uniform variable is not that cheap as one might think…



I’m lost. Right now I have one GLSLShaderObjectsState per object that I render, but it just feels wrong.

I would create a derived class of TriMesh and override in this class the draw function, so that you can set the right attribute for the current object right before it is rendered:


public void draw(Renderer r ) {
      shader.setUniform("foobar", 1);
      super.draw(r);
   }


When you want to reduce the setUniform-calls just attach all objects with the same attribute to a node and call setUniform for this parent-node in the draw-function of the node.

Since I want to set attribute pointers and no uniforms it would have to be…



public void draw(Renderer r ) {
    ((GLSLShaderObjectsState) states[RenderState.StateType.GLSLShaderObjects.ordinal()]).setAttributePointer("Tangent", 3, true, 0, tangentBuf);
    ((GLSLShaderObjectsState) states[RenderState.StateType.GLSLShaderObjects.ordinal()]).setAttributePointer("Binormal", 3, true, 0, binormalBuf);
    super.draw(r);
}



Wouldn't this also make a great addition to the TriMesh code. Since it already got a Tangent and Binormal property it would just be natural if it was also able to set the attribute pointers of an attached shader.

btw. subclassing TriMesh isn't really a good idea as all model loaders emit TriMesh objects. So I either (1) have to wrap the TriMesh inside a spatial, (2) hack the model loaders to not instanciate TriMesh objects directly but rather use a factory or (3) hack TriMesh and add the long missing feature of wiring extra vertex attributes to an attached shader.

All three sound promising, but I feel that (3) is the most simple and cleanest way.

Grouping all of your TriMeshes which need the same attributes under a node is no option?

JackNeil said:

Grouping all of your TriMeshes which need the same attributes under a node is no option?


No, as this would essentially break culling I'd say.


I just checked (3) and it seams that it would involve a rather small change of Geometry - not TriMesh, as Geometry is the class that defines those two fields.

I'd also propose to add two new properties "tangentAttributeName" and "binormalAttributeName" that define the name of the vertex attribute in the shader.


    /** The geometry's per vertex tangent information. */
    protected transient FloatBuffer tangentBuf;
   
    /** The attribute name of the geometry's per vertex tangent information */
    protected transient String tangentAttributeName = "Tangent";

    /** The geometry's per vertex binormal information. */
    protected transient FloatBuffer binormalBuf;
   
    /** The attribute name of the geometry's per vertex binormal information */
    protected transient String binormalAttributeName = "Binormal";

    // ...

    public void draw(Renderer r) {
       if (tangentBuf != null) {
          ((GLSLShaderObjectsState) states[RenderState.StateType.GLSLShaderObjects.ordinal()]).setAttributePointer("Tangent", 3, true, 0, tangentBuf);
       }
       if (binormalBuf != null) {
          ((GLSLShaderObjectsState) states[RenderState.StateType.GLSLShaderObjects.ordinal()]).setAttributePointer("Binormal", 3, true, 0, binormalBuf);
       }
    }

    // ...

    public void setTangentAttributeName(String name) {
       this.tangentAttributeName = name;
    }
   
    public String getTangentAttributeName() {
       return tangentAttributeName;
    }
   
    public void setBinormalAttributeName(String name) {
       this.binormalAttributeName = name;
    }
   
    public String getBinormalAttributeName() {
       return binormalAttributeName;
    }



btw. there is also a typo in the Javadoc of Geometry.java. It states "vertex color" and "vertex normal" for tangentBuf and binormalBuf.

Please also find a patch attached.

Is it possible that the geometry tangentBuf is not null but no GLSLShaderObjects RenderState is set?

That would result in a NPE no?



Please post the patch i the contribution depot, when its ready to be committed.


have a look at TestGLSLShaderDataLogic for one possible solution. ogrexml's animation package seems to use it in SkinningShaderLogic as well…

Core-Dump said:

Is it possible that the geometry tangentBuf is not null but no GLSLShaderObjects RenderState is set?
That would result in a NPE no?


You're absolutely right. Even though the the tangent and binormal buffer don't provide any mean without an attached shader it shouln't cause a NPE.

MrCoder said:

have a look at TestGLSLShaderDataLogic for one possible solution. ogrexml's animation package seems to use it in SkinningShaderLogic as well...


That looks like a rather nice way of doing it. So I came up with the following code snippet...


import com.jme.scene.Geometry;
import com.jme.scene.state.GLSLShaderDataLogic;
import com.jme.scene.state.GLSLShaderObjectsState;

public class TangentBinormalShaderDataLogic implements GLSLShaderDataLogic {

    public void applyData(GLSLShaderObjectsState shader, Geometry geom) {
        if (geom.getTangentBuffer() != null) {
            shader.setAttributePointer("Tangent", 3, true, 0, geom.getTangentBuffer());
       }
       if (geom.getBinormalBuffer() != null) {
            shader.setAttributePointer("Binormal", 3, true, 0, geom.getBinormalBuffer());
        }
    }

}



It works fine, though I wonder why it keeps warning me...


WARNING: User defined attributes might overwrite default OpenGL attributes
Jul 8, 2009 12:59:31 AM com.jme.scene.state.lwjgl.LWJGLShaderObjectsState checkAttributeSizeLimits


I really wonder why it keeps warning me about this. I checked LWJGLShaderObjectsState.checkAttributeSizeLimits to see where the error is coming from and see that it is caused by too many shader attributes being set. Since I just load a model with Vertex Position, Normal and TextureCoordinate attributes setting two extra vertex attributes shouln't trigger this warning, should it?


Besides I just stumbled accross an interresting statement in the Ogre3D documentation: "There are some drivers that do not behave correctly when mixing built-in vertex attributes like gl_Normal and custom vertex attributes, so for maximum compatibility you may well wish to use all custom attributes in shaders where you need at least one (e.g. for skeletal animation)." (See: http://www.ogre3d.org/docs/manual/manual_21.html#SEC113 - Binding vertex attributes)

Which gives me to think if it wouldn't be a good Idea to do it just the same and generally avoid the builtin attributes alltogether in favour of custom vertex attributes when working with shaders. What do you think?

I did ignore the last warning message, but just found out that it's far worse than expected. On my Linux box it messes up the whole rendering - hard to explain how it looks - it messes up the fixed pipeline rendering somehow as the scene gets rendered wrong even after the object with the shader is long gone / culled away.



Since I'm in a hurry to get some 3D renderings done for a presentation I picked a simple and ugly solution. Instead of specifying my own vertex attributes I set the TextureCoords for TextureUnit 1 and 2. Since I have 3 textures loaded those units also find their way into my shader code so it works as expected.



TangentBinormalGenerator.generate(model);
model.setTextureCoords(new TexCoords(model.getTangentBuffer(), 3), 1);
model.setTextureCoords(new TexCoords(model.getBinormalBuffer(), 3), 2);



I wouldn't call this pretty and I don't like the idea of using gl_MultiTexCoords1 and 2 for this, but it's the only that I was able to get this to work reliable. I think the method "shader.setAttributePointer" really needs a way to specify the attribute index to use. Or even better do it just like Ogre which specifies that uv6 and uv7 are shared with tangent and bitangent.

Edit: By accident I was calling getNormalBuffer instead of getTangentBuffer and getTangentBuffer instead of getBinormalBuffer. Just corrected that in the sample code.
mp said:

I did ignore the last warning message, but just found out that it's far worse than expected. On my Linux box it messes up the whole rendering - hard to explain how it looks - it messes up the fixed pipeline rendering somehow as the scene gets rendered wrong even after the object with the shader is long gone / culled away.


Same here, your solution works like a charm. Thx.  ;)

You can avoid storing the binormal for models, then you just need one texture coordinate unit. To compute the binormal, just take the cross product of the normal and tangent in the vertex shader.