📜 ⬆️ ⬇️

Debugging Shaders in Java + Groovy



Shader syntax highlighting. The relationship between shaders and external data structures. Unit tests for shaders, debugging, refactoring, static code analysis, and generally full support for IDE. How to get all this, what is the catch, and what to register in maven ...

Create a project.

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

We copy native libraries in a root of the project.
')
 $ mvn nativedependencies:copy 

This will allow us to run files that have the “main” method on 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 translated into plain glsl code. The shader in Groovy 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; for her, they are the usual Groovy and Java classes. As a result, we have the following amenities:


But even if you use other languages, you can still keep the shader 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 simply use auto-generated files with glsl code.

Specific example



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

Pixel shader


This is the usual 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 goes into the shader input (SpecularFi), and where the result should be written (StandardFrame). We also had to abandon the names of the form vec3, vec4, because Groovy could not make friends with the names of classes starting with a small letter.

 public class SpecularF extends FragmentShaderParent<SpecularFi> { 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 this.



We look that gets to an input of a pixel shader (ctrl + space).



Run the unit test and look at the calculations in the debug.



Input data for pixel shader


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 the pixel shader, this is a Groovy class, with uniform variables in the fields and the main method with explicit indication of the classes of incoming and outgoing data.

 class SpecularV extends VertexShaderParent<SpecularVi, SpecularFi> { 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 } } 

Vertex Shader Input


SpecularVi (vertex input). The class of the vertex shader falling into the input. It can also be used to fill the data buffer, the code of which, without the participation of the programmer, agrees 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); 

As you can see, their creation is the usual instantiation of classes. We leave the shader in variables in order to later transfer the data to them (degree of brilliance, direction of light, etc.)

Next, a buffer is created with the data. It uses the same class that the vertex shader entered.

 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 in the input data for shaders. Parameter passing is simply setting the field values ​​in the Groovy objects of the shaders (which were prudently available as 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, actually, the connection of the shader and the rendering.

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

Unit test shader.

 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 example 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:

  <dependencies> <dependency> <groupId>yk</groupId> <artifactId>senjin</artifactId> <version>0.11</version> </dependency> </dependencies> <repositories> <repository> <id>yk.senjin</id> <url>https://github.com/kravchik/mvn-repo/raw/master</url> </repository> </repositories> 

Well, briefly about the syntactic differences:

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

PS I develop the project spontaneously - something will be needed, it is just interesting to do something. Before I had time to make the structure and a lot of other things. If you need some kind of functionality or direction of development (android? Geometry shaders? Kotlin?) - contact, discuss!

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

Source: https://habr.com/ru/post/269591/


All Articles