Hello people
I don't know if you know the problem of modern OpenGl. In modern OpenGl you have to use shaders. And writing shaders itself is very nice, too, because the opengl shading language is really nice to write code in. But it is a huge pain to acutally use it especially for mall tasks like the hello triangle, because the overhead to bind the inputs, to the attributes and uniforms, and getting their locations is not only big, you can also very easily make something wrong, like for example forgetting to enable an attribute and as a result you only get a black screen and get frustrated pretty fast. Everything that I have seen so far, adresses this either by emulating you a fixed function pipeline, where you again do not write your shaders again (advantage of modern OpenGl is lost), or you a huge framework with a lot of predefined attributes and uniforms that you can use then. All of them are not really satisfying to me, so I came up with my solution to write a DSL in Nim, becaues C++ is not powerful enough for that. The current solution is just the first iteration that works, and that I would like share, but it is by far not finished, it can basically generate all your boilerplate code for an hello triangle, but it can't do advanced techniques like render to texture, or transform feedback, or even textures, but that stuff is planned. So take a look this is my render loop:
let glslCode = """
vec4 mymix(vec4 color, float alpha) {
float a = 3*(alpha/3 - floor(alpha/3));
float x = 1 - min(1, min(a, 3-a));
float y = 1 - min(1, abs(a - 1));
float z = 1 - min(1, abs(a - 2));
float r = dot(vec4(x,y,z,0), color);
float g = dot(vec4(y,z,x,0), color);
float b = dot(vec4(z,x,y,0), color);
return vec4(r,g,b, color.a);
}
"""
var projection_mat : Mat4x4[float64]
proc reshape(newWidth: cint, newHeight: cint) =
glViewport(0, 0, newWidth, newHeight) # Set the viewport to cover the new window
projection_mat = perspective(45.0, newWidth / newHeight, 0.1, 100.0)
var mouseX, mouseY: int32
var time = 0.0
proc render() =
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) # Clear color and depth buffers
var modelview_mat: Mat4x4[float] = I4()
modelview_mat = modelview_mat.transform( vec3(2*sin(time), 2*cos(time), -7.0) )
modelview_mat = modelview_mat.rotate( vec3[float](0,0,1), time*1.1 )
modelview_mat = modelview_mat.rotate( vec3[float](0,1,0), time*1.2 )
modelview_mat = modelview_mat.rotate( vec3[float](1,0,0), time*1.3 )
let mouseX_Norm = (mouseX.float32 / screenWidth.float32)
let mouseY_Norm = (mouseY.float32 / screenHeight.float32)
let mousePosNorm = vec2(mouseX_Norm, mouseY_Norm)
let mvp = modelview_mat * projection_mat;
shadingDsl:
uniforms:
modelview = modelview_mat
projection = projection_mat
time
mousePosNorm
attributes:
pos = vertex
col = color
varyings:
var v_col : vec4
frag_out:
var color : vec4
includes:
glslCode
vertex_prg:
"""
gl_Position = projection * modelview * vec4(pos,1);
v_col = vec4(col,1);
"""
fragment_prg:
"""
vec2 offset = gl_FragCoord.xy / 32 + mousePosNorm * 10;
color = mymix(v_col, time + dot( vec2(cos(time),sin(time)), offset ));
"""
glSwapWindow(window) # Swap the front and back frame buffers (double buffering)
the Dsl has two capture blocks, the uniforms, and the attributes block. In the uniforms block an assignment basically gives a symbol from the outer program a name in the glsl program. If there is just an identifyer then that symbol is captured without changing the name. Attributes work the same, just with the difference, that they expect seq[] parameters, and that at the moment they are only loaded once. The section vertex_prg and fragment_prg is just the part of the shader that is belongs in the main function, the rest like in and out variables are generated. Also all calls to write the uniforms are generated, enable the attributes, create the buffers, fill the buffers all of if is generated. Because shader code can't be compiled at compile time, the program gets compiled the first time the program reaches the shader.
my current problem has to do with typed subexpressions in the macro. Generally my dsl is a completely untyped tree, bus as you can see, that in the uniforms and attributes section, where the type depends on the outer context. Currently I have no idea, how I can get the type from within the macro.
shadingDsl:
uniforms: # this identifier doesn't exist, it's just expected from the macro
modelview = modelview_mat # what is the type of modelview_mat ?
projection = projection_mat # chat is the type of projection_mat ?
time # what is the type of time ?
mousePosNorm # what is the type of mousePosNorm ?
attributes:
pos = vertex # what is the type of vertex ?
col = color # what is the type of color ?
I need the type of the expression, because I need to generate shader code with type for each uniform and attribute. At the moment, I am generating code that generates the shader code at runtime, but I would prefer just for maintainability, to generate the shader directly in my macro.
The best and easiest solution would be, if I could call some functions, that tells me the type of an ast, if it would be evaluated i the calling context of the macro, but I haven't found anything like that yet.
Another solution would be, if my macro generates code, that calls a macro again, but this time with typed arguments, but here is the problem, that I need grouping. I need to distinguish arguments from the attributes section and uniforms section.
Didn't read the entire StackOverflow post, but here's the Nim equivalent of your C++ code:
type X[T] = object
template value(T:type X[int]): string = "int"
template value(T:type X[float]): string = "float"
echo X[int].value
echo X[float].value
thanks a lot. that's exactly what I needed.
EDIT: I think this is shorter and more suitable:
template value(T:type int): string = "int"
template value(T:type float): string = "float"
echo value(int)
echo value(float)
I just needed a way to pass a type at compile time
Some unproductive comment from me: Make it so that people can opt-in for a "Nim is king" solution. Instead of:
let glslCode = """
vec4 mymix(vec4 color, float alpha) {
float a = 3*(alpha/3 - floor(alpha/3));
float x = 1 - min(1, min(a, 3-a));
float y = 1 - min(1, abs(a - 1));
float z = 1 - min(1, abs(a - 2));
float r = dot(vec4(x,y,z,0), color);
float g = dot(vec4(y,z,x,0), color);
float b = dot(vec4(z,x,y,0), color);
return vec4(r,g,b, color.a);
}
"""
Also support:
proc mymix(color: vec4; alpha: float32): vec4 {.glslCode.} =
let
a = 3*(alpha/3 - floor(alpha/3))
x = 1 - min(1, min(a, 3-a))
y = 1 - min(1, abs(a - 1))
z = 1 - min(1, abs(a - 2))
r = dot(vec4(x,y,z,0), color)
g = dot(vec4(y,z,x,0), color)
b = dot(vec4(z,x,y,0), color)
return vec4(r,g,b, color.a)
here is an example, how I can use the geometry-shader, in order to draw face normals:
shadingDsl(GL_TRIANGLES, vertex.len.GLsizei):
uniforms:
modelview = modelview_mat
projection = projection_mat
attributes:
pos = vertex
normal
vertexMain:
"""
gl_Position = modelview * vec4(pos, 1);
v_eyepos = modelview * vec4(pos,1);
"""
vertexOut:
"out vec4 v_eyepos"
geometryMain:
"layout(line_strip, max_vertices=2) out"
"""
vec4 center = v_eyepos[0] + v_eyepos[1] + v_eyepos[2];
vec3 v1 = (v_eyepos[1] - v_eyepos[0]).xyz;
vec3 v2 = (v_eyepos[2] - v_eyepos[0]).xyz;
vec4 normal = vec4(cross(v1,v2),0);
gl_Position = projection * center;
EmitVertex();
gl_Position = projection * (center + normal);
EmitVertex();
"""
fragmentMain:
"""
color = vec4(1);
"""
fragmentOut:
"out vec4 color"
and here is a screenshot of how it looks like:
If you want to run this program, you should be able to do so by cloning, and running the example in the examples folder. But there is a bug in the glm dependency. If you see the compilatin crash, you have to replace four float types with the generic type T.