Debugging shaders in Java + Groovy



Shader syntax highlighting. The relationship between shaders and external data structures. Unit tests for shaders, debug, refactoring, static code analysis, and in general full support for IDEs. About how to get it all, what’s the catch, and what to write in the maven ...

We create a project.

$ git clone https://github.com/kravchik/senjin

Copy native libraries to the root of the project.

$ mvn nativedependencies:copy

This will allow us to run files that have the “main” method by Ctrl + Shift + F10 (in IDEA) directly from the editor window, without worrying about the classpath.

The library works like this: the shader code is written in Groovy, then it is translated into regular glsl code. Groovy shader is written as regular code that can be called from Java. The shader uses the same fields and classes as the main program. This allows the IDE to understand how the shader and the rest of the code are interconnected; they are just the usual Groovy and Java classes for it. As a result, we have the following amenities:

  • IDE support (refactoring, highlighting)
  • static analysis of simple and not very mistakes
  • shader debugging
  • unit tests for shaders
  • the relationship between the structures of buffers and shaders

But even if you use other languages, you can still keep shaders in Groovy and Java. You will not get a link to the rest of the project, but unit tests, debug, IDE support will be available. Then the main project will just use auto-generated files with glsl code.

Specific example



I’ll show you the main points using the example of a specular shader (rendering of “plastic material”) - it’s quite simple, but it uses varying, uniform, attributes, textures, there is mathematics, in general, you can touch the technology.

Pixel shader


This is a regular Groovy class with the main method. The standard opengl functions are inherited by the shader. Uniform variables are declared as shader fields. The shader code is in the main function, but its declaration is different from glsl - it explicitly indicates what gets to the input of the shader (SpecularFi), and where to write the result (StandardFrame). We also had to abandon the names of the vec3, vec4 type, since Groovy could not make friends with class names starting with a lowercase letter.

public class SpecularF extends FragmentShaderParent {
    public Sampler2D txt = new Sampler2D()
    public float shininess = 10;
    public Vec3f ambient = Vec3f(0.1, 0.1, 0.1);
    public Vec3f lightDir
    def void main(SpecularFi i, StandardFrame o) {
        Vec3f color = texture(txt, i.uv).xyz;
        Vec3f matSpec = Vec3f(0.6, 0.5, 0.3);
        Vec3f lightColor = Vec3f(1, 1, 1);
        Vec3f diffuse  = color * max(0.0, dot(i.normal, lightDir)) * lightColor;
        Vec3f r = normalize(reflect(normalize(i.csLightDir), normalize(i.csNormal)));
        Vec3f specular = lightColor * matSpec * pow(max(0.0, dot(r, normalize(i.csEyeDir))), shininess);
        o.gl_FragColor =  Vec4f(ambient + diffuse + specular, 1);
    }
}

Here you can already see the merits of the approach. We make a small mistake in the name and the IDE immediately reports it.



We look at what gets to the input of the pixel shader (ctrl + space).



We launch the unit test and look at the calculations in the debug.



Pixel Shader Input


SpecularFi (fragment input). A class containing data that is outgoing for a vertex shader and incoming for a pixel shader.

public class SpecularFi extends BaseVSOutput {
   public Vec3f normal;
   public Vec3f csNormal;//cam space normal
   public Vec3f csEyeDir;
   public Vec2f uv;
   public Vec3f csLightDir;//cam space light dir
}

Vertex shader


Like a pixel shader, this is a Groovy class, with uniform variables in the fields and the main method with an explicit indication of the classes of incoming and outgoing data.

class SpecularV extends VertexShaderParent {
   public Matrix3 normalMatrix;
   public Matrix4 modelViewProjectionMatrix;
   public Vec3f lightDir
   void main(SpecularVi i, SpecularFi o) {
       o.normal = i.normal
       o.csNormal = normalMatrix * i.normal
       o.gl_Position = modelViewProjectionMatrix * Vec4f(i.pos, 1)
       o.csEyeDir = o.gl_Position.xyz
       o.uv = i.uv
       o.csLightDir = normalMatrix * lightDir
   }
}

Input for vertex shader


SpecularVi (vertex input). The class entering the vertex shader. It can also be used to fill the data buffer, the code of which without the participation of the programmer will agree with the shader code (goodbye glGetAttribLocation, glBindBuffer, glVertexAttribPointer and other offal).

public class SpecularVi {
    public Vec3f normal;
    public Vec3f pos;
    public Vec2f uv;
}

Creating a vertex and pixel shader and combining them into a program:

SpecularF fragmentShader = new SpecularF();
SpecularV vertexShader = new SpecularV();
GShader shaderProgram = new GShader(vertexShader, fragmentShader);

Apparently, their creation is a usual instance of classes. We leave the shaders in the variables in order to later transmit data to them (the degree of brilliance, the direction of the light, etc.).

Next, a data buffer is created. It uses the same class that got to the input of the vertex shader.

ReflectionVBO vbo1 = new ReflectionVBO();
vbo1.bindToShader(shaderProgram);
vbo1.setData(al(
       new SpecularVi(v3(-5, -5, 0), v3(-1,-1, 1).normalized(), v2(0, 1)),
       new SpecularVi(v3( 5, -5, 0), v3( 1,-1, 1).normalized(), v2(1, 1)),
       new SpecularVi(v3( 5,  5, 0), v3( 1, 1, 1).normalized(), v2(1, 0)),
       new SpecularVi(v3(-5,  5, 0), v3(-1, 1, 1).normalized(), v2(0, 0))));
vbo1.upload();

Filling input for shaders. Passing parameters is simply setting the values ​​of the fields in the Groovy-objects of the shaders (which prudently remained available in the form of variables).

fragmentShader.shininess = 100;
vertexShader.lightDir = new Vec3f(1, 1, 1).normalized();
  //enable texture
texture.enable(0);
fragmentShader.txt.set(texture);
  //give data to shader
shaderProgram.currentVBO = vbo1;

And, in fact, connecting the shader and rendering.

shaderProgram.enable();
indices.draw();

Shader unit test.

f.main(vso, frame);
assertEquals(1, frame.gl_FragColor.w, 0.000001);
assertEquals(1 + 0.1 + 0.6, frame.gl_FragColor.x, 0.0001);
assertEquals(1 + 0.1 + 0.5, frame.gl_FragColor.y, 0.0001);
assertEquals(1 + 0.1 + 0.3, frame.gl_FragColor.z, 0.0001);

All sample code is here .

Test.java //простой юнит-тест шейдера
RawSpecular.java //простейший мейник создающий картинку для хабра
SpecularF.groovy //пиксельный шейдер
SpecularV.groovy //вертексный шейдер
SpecularVi.java //класс, описывающий вертекс (specular Vertex shader Input)
SpecularFi.java //класс, описывающий данные идущие из вертексного в пиксельный (specular Fragment shader Input) шейдер
WatchSpecular.java //более сложный мейник с кнопками, мышью, и прочим, усложняющим понимание и улучшающим экспириенс

The library is easy to connect via Maven:

yksenjin0.11yk.senjinhttps://github.com/kravchik/mvn-repo/raw/master

Well, briefly about the syntactic differences:

  1. Vec3f is used in the shader body instead of vec3 (the grooves could not be made friends with a class starting with a small letter)
  2. no uniform - instead of them just fields in the shader
  3. no varying, in, out - instead of them fields in classes passed to main

PS I am developing the project spontaneously - it will need something, it’s just interesting to do something. Until I managed to make structures and much more. If you need some kind of functionality or development direction (android? Geometry shaders? Kotlin?) - contact, we will discuss!

I also want to thank oshyshko and olexiy for their help in writing this article.

Also popular now: