Search code examples
libgdxshadercustomizationvertex-shader

Libgdx custom shader per-vertex attribute


After several days of struggling I came here. I'm trying to pass a custom per-vertex vec3 attribute to a custom shader based on this tutorial. The tutorial describes how to pass a custom uniform which actually works fine. However when I'm trying to modify the code to pass my custom per-vertex attribute it seems that nothing is transferred to vertex shader and I can't figure out how to make it to work.

So far I've done the following:

I've created several boxes with modelBuilder.createBox() (so I know for sure every model has 24 vertexes)

Then I'v generated a FloatBuffer containing actual attribute data like this:

int[] data = new int[]{x1, y1, z1, x1, y1, z1, ...}

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(data.length * 4);
byteBuffer.order(ByteOrder.nativeOrder());
mAttributeBuffer = byteBuffer.asFloatBuffer();
mAttributeBuffer.put(data);
mAttributeBuffer.position(0); 

Then I'm initializing the corresponding attribute location variable (successfully, a_coord >= 0):

a_coord = program.getAttributeLocation("a_coord");

After that on libgdx side in custom shader's render(Renderable) method I'm passing the buffer to OpenGL like this:

program.setVertexAttribute(a_coord, 3, Gdx.gl20.GL_FLOAT, false, 0, mAttributeBuffer);

My custom vertex shader is as the following:

attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord0;
    
uniform mat4 u_worldTrans;
uniform mat4 u_projTrans;


varying vec2 v_texCoord0;

//my custom attribute
attribute vec2 a_coord;

void main() {
    v_texCoord0 = a_texCoord0;
    float posY =  a_position.y + a_coord.y;
    gl_Position = u_projTrans * u_worldTrans * vec4(a_position.x, posY, a_position.z, 1.0);
}

The problem

At the moment a_coord is 0 for every vertex. What am I missing and how to correctly pass custom attribute to vertex shader?

I'm guessing the problem is somewhere in VBO field and the way libGDX passes attribute data to vertexes but I still can't figure out how to make it work.

I'll be glad if anyone can point me in the right direction on this question.

Complete code:

Main AplicationListener class:

public class ProtoGame implements ApplicationListener {

    public ProtoGame()
    {
        super();
    }

    public PerspectiveCamera cam;
    public CameraInputController camController;
    public Model model;
    public Array<ModelInstance> instances = new Array<ModelInstance>();
    public ModelBatch modelBatch;

    @Override
    public void create () {
        cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        cam.position.set(0f, 8f, 8f);
        cam.lookAt(0,0,0);
        cam.near = 1f;
        cam.far = 300f;
        cam.update();

        camController = new CameraInputController(cam);
        Gdx.input.setInputProcessor(camController);

        ModelBuilder modelBuilder = new ModelBuilder();
        model = modelBuilder.createBox(1f, 1f, 1f,
                new Material(),
                VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal | VertexAttributes.Usage.TextureCoordinates);

        Color colorU = new Color(), colorV = new Color();
        for (int x = -5; x <= 5; x+=2) {
            for (int z = -5; z<=5; z+=2) {
                ModelInstance instance = new ModelInstance(model, x, 0, z);
                //this is where I'll put per-vertex attribute data for every instance
                //but for now it's hardcoded in the Shader class so the data is the same across instances  

                TestShader.DoubleColorAttribute attr = new TestShader.DoubleColorAttribute(TestShader.DoubleColorAttribute.DiffuseUV,
                        colorU.set((x+5f)/10f, 1f - (z+5f)/10f, 0, 1),
                        colorV.set(1f - (x+5f)/10f, 0, (z+5f)/10f, 1));
                instance.materials.get(0).set(attr);
                instances.add(instance);
            }
        }


        modelBatch = new ModelBatch(new BaseShaderProvider() {

            @Override
            protected Shader createShader(Renderable renderable) {
                return new TestShader();
            }

        });
    }

    @Override
    public void render () {
        camController.update();

        Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

        modelBatch.begin(cam);
        for (ModelInstance instance : instances)
            modelBatch.render(instance);
        modelBatch.end();
    }

    @Override
    public void dispose () {
        model.dispose();
        modelBatch.dispose();
    }
}

Custom libgdx shader class:

public class TestShader implements Shader {
    private FloatBuffer mAttributeBuffer;



    ShaderProgram program;
    Camera camera;
    RenderContext context;
    int u_projTrans;
    int u_worldTrans;
    int u_colorU;
    int u_colorV;

    int a_coord;

    private static String getCustomVertexShader() {
        return Gdx.files.internal("shader/test.vertex.glsl").readString();
    }

    private static String getCustomFragmentShader() {
        return Gdx.files.internal("shader/test.fragment.glsl").readString();
    }


    @Override
    public void init() {

        program = new ShaderProgram(getCustomVertexShader(), getCustomFragmentShader());
        if (!program.isCompiled())
            throw new GdxRuntimeException(program.getLog());

        //tutorial's logic to init custom uniform locations
        u_projTrans = program.getUniformLocation("u_projTrans");
        u_worldTrans = program.getUniformLocation("u_worldTrans");
        u_colorU = program.getUniformLocation("u_colorU");
        u_colorV = program.getUniformLocation("u_colorV");

        //initing custom attribute location
        a_coord = program.getAttributeLocation("a_coord");


        //generating data and passing it to nio Buffer
        float data[] = generateData();

        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(data.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
        mAttributeBuffer = byteBuffer.asFloatBuffer();
        mAttributeBuffer.put(data);
        mAttributeBuffer.position(0);
    }

    private float[] generateData() {
        Vector3[] dataArray = new Vector3[1];
        dataArray[0] = new Vector3(2, 2, 2);

        int components = 3;
        int vertexPerModel = 24;
        float[] data = new float[dataArray.length * components  * vertexPerModel];
        for(int i = 0; i < dataArray.length; ++i){
            int i3 = i * components;
            for(int j = 0; j < vertexPerModel; ++j) {
                int j3 = j * components;
                data[i3 + 0 + j3] = dataArray[i].x;
                data[i3 + 1 + j3] = dataArray[i].y;
                data[i3 + 2 + j3] = dataArray[i].z;
            }
        }
        return data;
    }

    @Override
    public void dispose() {
        program.dispose();
    }

    @Override
    public void begin(Camera camera, RenderContext context) {
        this.camera = camera;
        this.context = context;
        program.begin();
        program.setUniformMatrix(u_projTrans, camera.combined);
        context.setDepthTest(GL20.GL_LEQUAL);
        context.setCullFace(GL20.GL_BACK);
    }

    @Override
    public void render(Renderable renderable) {
        program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
        //tutorial's logic to pass uniform
        DoubleColorAttribute attribute = ((DoubleColorAttribute) renderable.material.get(DoubleColorAttribute.DiffuseUV));
        program.setUniformf(u_colorU, attribute.color1.r, attribute.color1.g, attribute.color1.b);
        program.setUniformf(u_colorV, attribute.color2.r, attribute.color2.g, attribute.color2.b);


        //passing my custom attributes to the vertex shader
        program.setVertexAttribute(a_coord, 3, Gdx.gl20.GL_FLOAT, false, 0, mAttributeBuffer);


        renderable.mesh.render(program, renderable.primitiveType,
                renderable.meshPartOffset, renderable.meshPartSize);
    }

    @Override
    public void end() {
        program.end();
    }

    @Override
    public int compareTo(Shader other) {
        return 0;
    }

    @Override
    public boolean canRender(Renderable renderable) {
        return renderable.material.has(DoubleColorAttribute.DiffuseUV);
    }
}

Solution

  • Finally I was able to pass a custom attribute to vertex shader! Thanks a lot to @Xoppa for pointing me in the right direction.

    This is the working solution I've got so far (I'm open for any further advices on how to implement it in a more elegant way):

    First of all, as Xoppa stated in the comment it's required to create a model providing custom vertex structure while building it. So model creation may look like this:

    VertexAttribute posAttr = new VertexAttribute(VertexAttributes.Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE);
    ...
    VertexAttribute customVertexAttr = new VertexAttribute(512, 3, "a_custom");
    VertexAttributes vertexAttributes = new VertexAttributes(
            posAttr,
            ...
            customVertexAttr);
    
    ModelBuilder modelBuilder = new ModelBuilder();
    modelBuilder.begin();
    modelBuilder.
            part("box", GL20.GL_TRIANGLES, vertexAttributes, new Material()).
            box(1f, 1f, 1f);
    model = modelBuilder.end();
    

    Or the same with MeshBuilder:

    MeshBuilder meshBuilder = new MeshBuilder();
    VertexAttributes vertexAttributes = new VertexAttributes(...);
    meshBuilder.begin(vertexAttributes);
    meshBuilder.part("box", GL20.GL_TRIANGLES);
    meshBuilder.setColor(color);
    meshBuilder.box(1f, 1f, 1f);
    Mesh mesh = meshBuilder.end();
    

    This code will create model with vertices containing additional data according to the provided attributes. It's time to fill the corresponding vertex array. You need a mesh for this - it stores vertices array - a flat array of packed attributes one after another vertex by vertex. So what you need is a number of attributes per vertex as well as an offset for attribute which needs to be modified. Mesh stores all that data:

    Mesh mesh = model.meshes.get(0);
    int numVertices = mesh.getNumVertices();
    // vertex size and offset are in byte so we need to divide it by 4
    int vertexSize = mesh.getVertexAttributes().vertexSize / 4;
    //it's possible to use usage int here passed previously to VertexAttribute constructor. 
    VertexAttribute customAttribute = mesh.getVertexAttribute(512)
    int offset = customAttribute.offset / 4;
    
    float[] vertices = new float[numVertices * vertexSize];
    mesh.getVertices(vertices);
    

    We are ready to pass the data:

    List<Vector3> customData ...
    
    for(int i = 0; i < numVertices; ++i){
        int index = i * vertexSize + offset;
        vertices[index + 0] = customData.get(i).x;
        vertices[index + 1] = customData.get(i).y;
        vertices[index + 2] = customData.get(i).z;
    }
    

    And don't forget to pass the updated vertices array back to the mesh:

    mesh.updateVertices(0, vertices);
    

    That's it.

    Here's also an implementation of a helper method to create a mix of default attributes using Usage flags alongside with custom attributes:

    private VertexAttributes createMixedVertexAttribute(int defaultAtributes, List<VertexAttribute> customAttributes){
        VertexAttributes defaultAttributes = MeshBuilder.createAttributes(defaultAtributes);
        List<VertexAttribute> attributeList = new ArrayList<VertexAttribute>();
        for(VertexAttribute attribute: defaultAttributes){
            attributeList.add(attribute);
        }
        attributeList.addAll(customAttributes);
        VertexAttribute[] typeArray = new VertexAttribute[0];
        VertexAttributes mixedVertexAttributes = new VertexAttributes(attributeList.toArray(typeArray));
        return mixedVertexAttributes;
    }
    

    The full source:

    public class ProtoGame implements ApplicationListener {
        
        private static final int CUSTOM_ATTRIBUTE_USAGE = 512;
    
        public ProtoGame()
        {
            super();
        }
    
        public PerspectiveCamera cam;
        public CameraInputController camController;
        public Model model;
        public Array<ModelInstance> instances = new Array<ModelInstance>();
        public ModelBatch modelBatch;
    
        @Override
        public void create () {
            cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
            cam.position.set(0f, 8f, 8f);
            cam.lookAt(0, 0, 0);
            cam.near = 1f;
            cam.far = 300f;
            cam.update();
    
            camController = new CameraInputController(cam);
            Gdx.input.setInputProcessor(camController);
    
    
            Model model = createModelWithCustomAttributes();
            Mesh mesh = model.meshes.get(0);
            setCustomAttributeData(mesh);
    
    
            Color colorU = new Color(), colorV = new Color();
            for (int x = -5; x <= 5; x+=2) {
                for (int z = -5; z<=5; z+=2) {
                    ModelInstance instance = new ModelInstance(model, x, 0, z);
                    TestShader.DoubleColorAttribute attr = new TestShader.DoubleColorAttribute(TestShader.DoubleColorAttribute.DiffuseUV,
                            colorU.set((x+5f)/10f, 1f - (z+5f)/10f, 0, 1),
                            colorV.set(1f - (x+5f)/10f, 0, (z+5f)/10f, 1));
                    instance.materials.get(0).set(attr);
                    instances.add(instance);
                }
            }
    
    
            modelBatch = new ModelBatch(new BaseShaderProvider() {
    
                @Override
                protected Shader createShader(Renderable renderable) {
                    return new TestShader();
                }
    
            });
        }
    
    
    
    
        @Override
        public void render () {
            camController.update();
    
            Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
            Gdx.gl.glClearColor(1, 1, 1, 1);
            Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
    
            modelBatch.begin(cam);
            for (ModelInstance instance : instances)
                modelBatch.render(instance);
            modelBatch.end();
        }
    
        private Model createModelWithCustomAttributes() {
            int defaultAttributes = VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal | VertexAttributes.Usage.TextureCoordinates;
            VertexAttribute customVertexAttr = new VertexAttribute(CUSTOM_ATTRIBUTE_USAGE, 3, "a_custom");
    
            List<VertexAttribute> customAttributeList = new ArrayList<VertexAttribute>();
            customAttributeList.add(customVertexAttr);
    
            VertexAttributes vertexAttributes = createMixedVertexAttribute(defaultAttributes, customAttributeList);
    
            ModelBuilder modelBuilder = new ModelBuilder();
            modelBuilder.begin();
            modelBuilder.
                    part("box", GL20.GL_TRIANGLES, vertexAttributes, new Material()).
                    box(1f, 1f, 1f);
            return modelBuilder.end();
        }
    
        private void setCustomAttributeData(Mesh mesh) {
            int numVertices = mesh.getNumVertices();
    
            int vertexSize = mesh.getVertexAttributes().vertexSize / 4;
            int offset = mesh.getVertexAttribute(CUSTOM_ATTRIBUTE_USAGE).offset / 4;
    
            float[] vertices = new float[numVertices * vertexSize];
            mesh.getVertices(vertices);
    
            for(int i = 0; i < numVertices; ++i){
                int index = i * vertexSize + offset;
                vertices[index + 0] = i;
                vertices[index + 1] = i;
                vertices[index + 2] = i;
            }
            mesh.updateVertices(0, vertices);
        }    
    
        @Override
        public void dispose () {
            model.dispose();
            modelBatch.dispose();
        }
    
        private VertexAttributes createMixedVertexAttribute(int defaultAtributes, List<VertexAttribute> customAttributes){
            VertexAttributes defaultAttributes = MeshBuilder.createAttributes(defaultAtributes);
            List<VertexAttribute> attributeList = new ArrayList<VertexAttribute>();
            for(VertexAttribute attribute: defaultAttributes){
                attributeList.add(attribute);
            }
            attributeList.addAll(customAttributes);
            VertexAttribute[] typeArray = new VertexAttribute[0];
            VertexAttributes mixedVertexAttributes = new VertexAttributes(attributeList.toArray(typeArray));
            return mixedVertexAttributes;
        }
    
    
        @Override
        public void resize(int width, int height) {
        }
    
        @Override
        public void pause() {
        }
    
        @Override
        public void resume() {
        }
    }
    

    Vertex shader:

    attribute vec3 a_position;
    attribute vec3 a_normal;
    attribute vec2 a_texCoord0;
        
    uniform mat4 u_worldTrans;
    uniform mat4 u_projTrans;
    
    
    varying vec2 v_texCoord0;
    
    attribute vec3 a_custom;
    
    void main() {
        v_texCoord0 = a_texCoord0;
        float posX =  a_position.x + a_custom.x;
        float posY =  a_position.y + a_custom.y;
        float posZ =  a_position.z + a_custom.z;
        gl_Position = u_projTrans * u_worldTrans * vec4(posX, posY, posZ, 1.0);
    }
    

    Fragment shader

    #ifdef GL_ES 
    precision mediump float;
    #endif
        
    uniform vec3 u_colorU;
    uniform vec3 u_colorV;
        
    varying vec2 v_texCoord0;
        
    void main() {
        gl_FragColor = vec4(v_texCoord0.x * u_colorU + v_texCoord0.y * u_colorV, 1.0);
    }