Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
moderngl vbo test
#!/usr/bin/env python
import gi
gi.require_version('Gtk', '3.0'); from gi.repository import Gtk, Gdk, GLib
import moderngl
import struct
VERTEX_SHADER = """
#version 400
/** Shader for drawing lines of varying thickness,
* including bezier curves.
* Used with lines.geom.
*/
in vec3 in_p0;
in vec3 in_p1;
in vec3 in_c0;
in vec3 in_c1;
//input attributes
out LINE_POINTS {
vec3 p0, p1, c0, c1;
} v_point;
void main() {
v_point.p0 = in_p0;
v_point.p1 = in_p1;
v_point.c0 = in_c0;
v_point.c1 = in_c1;
}
"""
GEOMETRY_SHADER = """
#version 420
#define PI 3.141592654
/** Shader for drawing lines of varying thickness,
* including bezier curves.
* Used with lines.vert.
*/
//we consume points (in groups of 4) and produce quads (the lines)
//max_vertices is for the entire shader, not per primitive
//each line has 4 vertices per segment
layout(points) in;
layout(triangle_strip, max_vertices=256) out;
//input from vertex shader
in LINE_POINTS {
vec3 p0, p1, c0, c1;
} v_point[];
//outputs to fragment shader
out vec4 fragColor; //fragment color
out vec2 fragTexCoord; //fragment texture coordinates
//transformation matrices
uniform mat4 matProjection; //projection matrix
uniform mat4 matModelview; //modelview matrix
//types used internally
struct Point {
/** Describes one point of a line.
*/
vec4 pos; //The point's coords
vec4 color; //The point's color
vec2 texCoord; //The point's texture coord
float lineWidth; //The line width at this point
};
vec2 rotate2d(vec2 vector, float angle) {
/** Rotate 2D vector.
* vector: Input vector.
* angle: Rotation angle in radians.
* Returns `vector` rotated by `angle`.
*/
return mat2(
cos(angle), -sin(angle),
sin(angle), cos(angle)
) * vector;
}
vec4 toBezier(float delta, int i, vec4 P0, vec4 P1, vec4 P2, vec4 P3) {
/** Given start, end, and control points,
* return coord of point along bezier curve.
* The curve goes from P0, near P1 and P2, to P3.
* delta: 1.0 / nSegments
* i: segment index
* PO: start of line
* P1: start control point
* P2: end control point
* P3: end of line
* Returns coords of segment `i`.
* from https://vicrucann.github.io/tutorials/bezier-shader/
*/
float t = delta * float(i);
float t2 = t * t;
float one_minus_t = 1.0 - t;
float one_minus_t2 = one_minus_t * one_minus_t;
return (P0 * one_minus_t2 * one_minus_t + P1 * 3.0 * t * one_minus_t2 + P2 * 3.0 * t2 * one_minus_t + P3 * t2 * t);
}
void drawVtx(vec4 pos, vec4 color, vec2 texCoord) {
/** Draw one vertex.
* pos: Vertex coordinates.
* color: Vertex color.
* texCoord: Vertex texture coordinate.
*/
gl_Position = matProjection * matModelview * pos;
fragTexCoord = texCoord;
fragColor = color;
EmitVertex();
}
void drawCircle(vec4 pos, float radius, int nVtxs) {
/** Draw a circle.
* pos: Coordinates of the circle's centre.
* radius: Circle's radius.
* nVtxs: Number of vertices to draw. Higher values
* produce a smoother circle but take longer to draw.
* Very rough circles may have a visible gap at one
* vertex due to drawing in triangle_strip mode.
*/
//subtract 1 so that we close the circle
//(this is number of vertices, not number of segments)
float d = (2.0 * PI) / float(nVtxs-1);
for(int i=0; i<nVtxs; i++) {
float t = d * i;
float x = pos.x + (radius * cos(t));
float y = pos.y + (radius * sin(t));
drawVtx(
vec4(x, y, pos.zw),
vec4(x, y, pos.zw),
vec2(x, y)
);
}
EndPrimitive();
}
void drawPoint(vec4 pos, vec4 color, float size) {
/** Draw a square point. Mainly used for debugging.
* pos: Coordinates of the point's centre.
* color: Color of the point.
* size: Width and height of the point.
*/
float s = size / 2.0;
drawVtx(vec4(pos.x-s, pos.y-s, pos.zw), color, vec2(0,0));
drawVtx(vec4(pos.x-s, pos.y+s, pos.zw), color, vec2(0,0));
drawVtx(vec4(pos.x+s, pos.y-s, pos.zw), color, vec2(0,0));
drawVtx(vec4(pos.x+s, pos.y+s, pos.zw), color, vec2(0,0));
EndPrimitive();
}
void drawLine(Point p0, Point p1) {
/** Draw a line of arbitrary thickness.
* p0: Start point.
* p1: End point.
* The points define coordinate, color, and texture coord.
*/
//compute angle of line
vec2 d = p1.pos.xy - p0.pos.xy;
float theta = atan(d.y, d.x);
//rotate by 90 degrees and move by lineWidth
//so that the original coord is on the midpoint of the edge
//of a rect from p0 to p1
float a = mod((theta + (PI / 2.0)), (2.0 * PI));
vec2 offset0 = rotate2d(vec2(p0.lineWidth / 2, 0), -a);
vec2 offset1 = rotate2d(vec2(p1.lineWidth / 2, 0), -a);
vec4 corner0 = vec4(p0.pos.xy + offset0, p0.pos.zw);
vec4 corner1 = vec4(p0.pos.xy - offset0, p0.pos.zw);
vec4 corner2 = vec4(p1.pos.xy + offset1, p1.pos.zw);
vec4 corner3 = vec4(p1.pos.xy - offset1, p1.pos.zw);
//draw rect between the corners
//swapping the colors/texcoords of corners 1 and 2 means
//the gradient is perpendicular to the segment,
//instead of parallel.
drawVtx(corner0, p0.color, p0.texCoord);
drawVtx(corner1, p1.color, p1.texCoord);
drawVtx(corner2, p0.color, p0.texCoord);
drawVtx(corner3, p1.color, p1.texCoord);
//EndPrimitive();
//don't end primitive; the line looks better if it's one
//long triangle strip instead of independent rects.
}
void main() {
drawCircle(vec4(100, 100, -1, 1), 8, 8);
//for(int i = 0; i < gl_in.length(); i++) { //for each line
for(int i = 0; i < 4; i++) { //for each line
vec4 p0 = vec4(v_point[i].p0.xyz, 1); //start point
vec4 p1 = vec4(v_point[i].p1.xyz, 1); //end point
vec4 c0 = vec4(v_point[i].c0.xyz, 1); //control point 0
vec4 c1 = vec4(v_point[i].c1.xyz, 1); //control point 1
drawCircle(vec4(100 + (10 * i), 100, -1, 1), 8, 8);
//draw the lines
//XXX this can have weird looking gaps between the
//rects if the line is very thick
int nSegments = 8;
float delta = 1.0 / nSegments;
vec4 prev = toBezier(delta, 0, p0, c0, c1, p1);
for(int seg=1; seg <= nSegments; seg++) {
vec4 cur = toBezier(delta, seg, p0, c0, c1, p1);
Point pA = Point(prev, vec4(1,0,0,1), vec2(0,0), 32);
Point pB = Point(cur, vec4(0,1,0,1), vec2(0,0), 32);
drawLine(pA, pB);
prev = cur;
}
EndPrimitive();
//draw the control points and connections to them
//unfortunately we can't switch to GL_LINE_LOOP for these,
//as geometry shaders can only output one primitive type.
//I didn't feel it necessary to create another whole
//shader just for these, so we'll just deal with overly
//detailed lines instead.
drawCircle(c0, 8, 12);
drawCircle(c1, 8, 12);
drawCircle(p0, 8, 12);
drawCircle(p1, 8, 12);
drawLine(
Point(p0, vec4(1,1,1,1), vec2(0,0), 1),
Point(c0, vec4(1,1,1,1), vec2(0,0), 1));
EndPrimitive();
drawLine(
Point(p1, vec4(1,1,1,1), vec2(0,0), 1),
Point(c1, vec4(1,1,1,1), vec2(0,0), 1));
EndPrimitive();
}
}
"""
FRAGMENT_SHADER = """
#version 400
uniform bool enableTexture = false;
uniform float minAlpha = 0.0; //discard texels where alpha < minAlpha
uniform vec4 modColor = vec4(1,1,1,1); //multiply all colors by this
uniform sampler2D inTexture;
in vec4 fragColor; //set by vertex shader
in vec2 fragTexCoord;
out vec4 outputColor; //the resulting fragment color
void main () {
vec4 color = fragColor;
if(enableTexture) {
color *= texture2D(inTexture, fragTexCoord.st).rgba;
}
color *= modColor;
if(color.a < minAlpha) discard;
outputColor = color;
//outputColor = gl_FragCoord;
//outputColor = vec4(1, 0, 0, 1);
}
"""
def checkError(obj, where):
err = obj.ctx.error
if err is not None and err != "GL_NO_ERROR":
print("Error:", where, err)
def makePerspectiveMatrix(left, right, bottom, top, near, far):
#names from glFrustum doc
W = (2*near) / (right-left)
H = (2*near) / (top-bottom)
A = (right+left) / (right-left)
B = (top+bottom) / (top-bottom)
C = -((far+near) / (far-near))
D = -((2*far*near) / (far-near))
return (
W, 0, 0, 0,
0, H, 0, 0,
A, B, C, -1,
0, 0, D, 0)
def makeIdentityMatrix(size):
data = []
for y in range(size):
for x in range(size):
data.append(1 if x == y else 0)
return tuple(data)
class GLContext:
def __init__(self, widget):
"""Create context for GTK widget."""
# create GL context
self.widget = widget
ctx = widget.get_window().create_gl_context()
ctx.set_required_version(4, 0)
ctx.set_debug_enabled(True)
ctx.set_use_es(0) # 1=yes 0=no -1=auto
print("ctx realize:", ctx.realize())
ctx.make_current()
# keep a reference to this so that it isn't garbage collected,
# or else we segfault.
self._gtk_ctx = ctx
# create ModernGL context and print info
self.ctx = moderngl.create_context()
#for k, v in self.ctx.info.items(): # dump info
# print(k, v)
print("version", self.ctx.version_code)
widget.connect('configure-event', self._on_configure_event)
def __getattr__(self, name):
try: val = self.__dict__[name]
except KeyError:
ctx = self.__dict__['ctx']
return getattr(ctx, name)
def loadProgram(self, **files):
"""Load a shader program.
files: Paths to shader code files; valid keys:
- fragment_shader
- vertex_shader
- geometry_shader
- XXX others?
Returns a Program.
"""
shaders = {}
for name, path in files.items():
#with open(path, 'rt') as file:
# shaders[name] = file.read()
shaders[name] = path
return self.ctx.program(**shaders)
def _on_configure_event(self, widget, event):
"""Callback for GTK configure-event on widget."""
self._updateViewport()
def _updateViewport(self):
"""Called when widget is created or resized, to update the
GL viewport to match its dimensions.
"""
rect = self.widget.get_allocation()
pos = self.widget.translate_coordinates(
self.widget.get_toplevel(), 0, 0)
if pos is None: return # widget is not visible
self.ctx.viewport = (pos[0], pos[1], rect.width, rect.height)
self._width, self._height = rect.width, rect.height
print("Viewport:", pos[0], pos[1], rect.width, rect.height)
class Program:
"""Base class for shader programs."""
def __init__(self, ctx):
self.ctx = ctx
@property
def memUsedCpu(self):
"""Estimated amount of CPU-side memory used."""
return None # amount not known
@property
def memUsedGpu(self):
"""Estimated amount of GPU-side memory used."""
return None # amount not known
def run(self, *args, **kwargs):
"""Execute the program."""
raise NotImplementedError
class LineDraw(Program):
"""Line drawing shader program.
Draws lines of arbitrary thickness, including bezier curves.
"""
MAX_POINTS = 1024
POINT_FMT = '3f'
LINE_FMT = '4i'
def __init__(self, ctx):
super().__init__(ctx)
# load shaders
self.program = self.ctx.loadProgram(
vertex_shader = VERTEX_SHADER,
geometry_shader = GEOMETRY_SHADER,
fragment_shader = FRAGMENT_SHADER,
)
# vtx -> tesselate -> geom -> fragment
# set up some parameters in the shader
self.program['matModelview'] .value = makeIdentityMatrix(4)
self.program['enableTexture'].value = False
self.program['minAlpha'] .value = 0.5
self.program['modColor'] .value = (1.0, 1.0, 1.0, 1.0)
# make vertex buffer/attribute objects to hold the line data
self.pointDataSize = struct.calcsize(self.POINT_FMT)
self.lineDataSize = struct.calcsize(self.LINE_FMT)
self.vboVtxs = self.ctx.buffer( # the actual vertices
reserve=self.MAX_POINTS * self.pointDataSize,
dynamic=True,
)
self.iboLines = self.ctx.buffer( # vtx index buffer
reserve=self.MAX_POINTS * self.lineDataSize,
dynamic=True,
)
self.vao = self.ctx.vertex_array(self.program,
[ # inputs to the vertex shader
(self.vboVtxs, '3f', 'in_p0'),
(self.vboVtxs, '3f', 'in_p1'),
(self.vboVtxs, '3f', 'in_c0'),
(self.vboVtxs, '3f', 'in_c1'),
],
#self.iboLines,
None,
)
#self.vboVtxs.bind_to_uniform_block(
# self.program['vertices'].location)
def setVertices(self, idx, *points):
"""Change one or more vertices in the buffer.
idx: Point index to set in the buffer.
points: Point to write.
"""
if idx < 0: idx = self.MAX_POINTS + idx
if idx < 0 or idx + len(points) >= self.MAX_POINTS:
raise IndexError(idx)
data = []
for p in points:
data.append(struct.pack(self.POINT_FMT, *p))
self.vboVtxs.write(b''.join(data),
offset = idx * self.pointDataSize)
def setLines(self, idx, *lines):
"""Change one or more lines in the buffer.
idx: Line to set.
lines: Vertex indices to write. (p0, p1, c0, c1)
"""
# XXX should we be using MAX_POINTS here?
if idx < 0: idx = self.MAX_POINTS + idx
if idx < 0 or idx + len(lines) >= self.MAX_POINTS:
raise IndexError(idx)
data = []
for line in lines:
p0, p1, c0, c1 = line
if c0 is None: c0 = p0
if c1 is None: c1 = c0
#p0 = 3
#p1 = 1
#c0 = 2
#c1 = 0
data.append(struct.pack(self.LINE_FMT, p0, p1, c0, c1))
self.iboLines.write(b''.join(data),
offset = idx * self.lineDataSize)
def run(self):
"""Draw the lines."""
#data = self.iboLines.read()
#dump = []
#for i in range(0, 0x100, 16):
# line = "%04X " % i
# for j in range(16):
# if (j&3) == 0: line += ' '
# line += "%02X " % data[i+j]
# dump.append(line)
#print("index buffer (obj %d):\n%s" % (
# self.iboLines.glo,
# '\n'.join(dump),
#))
checkError(self, "run 1")
# update projection matrix to current viewport
x, y, width, height = self.ctx.viewport
self.program['matProjection'].value = \
makePerspectiveMatrix(0, width, height, 0, 1, 100)
checkError(self, "run 2")
p0 = self.program['in_p0'].location
p1 = self.program['in_p1'].location
c0 = self.program['in_c0'].location
c1 = self.program['in_c1'].location
vbo = self.vboVtxs
checkError(self, "run 3")
print("p0=", p0, "p1=", p1, "c0=", c0, "c1=", c1)
self.vao.bind(p0, 'f', vbo, '3f', offset=0, stride=12*4)
checkError(self, "bind 1")
self.vao.bind(p1, 'f', vbo, '3f', offset=12)
self.vao.bind(c0, 'f', vbo, '3f', offset=24)
self.vao.bind(c1, 'f', vbo, '3f', offset=36)
checkError(self, "run 4")
self.vao.render(mode=moderngl.POINTS, vertices=4, instances=4)
checkError(self, "run 5")
class MyGLArea(Gtk.DrawingArea):
"""A GTK widget that displays GL rendering results.
This doesn't use GtkGlArea, because that creates its own
framebuffer which then makes it difficult to display the
results of our rendering. Instead it uses GtkDrawingArea,
manually creates a GL context for it, and renders to it.
"""
def __init__(self):
self.ctx = None
super().__init__()
self.bgColor = (0.0, 0.5, 0.5, 0.0) # r, g, b, a
self.connect("realize", self.on_realize)
self.connect("draw", self.on_draw)
def on_realize(self, area) -> None:
"""Called when the widget is being realized (ie initialized).
area: The widget itself.
"""
self.ctx = GLContext(self)
self.lineDraw = LineDraw(self.ctx)
self.ctx._updateViewport()
self.lineDraw.setVertices(0,
# p0, p1, c0, c1
( 32, 64, -1),
(640, 640, -1),
( 32, 640, -1),
(640, 64, -1),
)
self.lineDraw.setLines(0,
(0, 1, None, None),
)
#svg = SvgParser().parse("data/tests/path.svg")
#path = svg.elements[0]
#self.lineDraw.setVertices(0, *path.vertices)
#self.lineDraw.setLines (0, *path.lines)
def on_draw(self, area, cr) -> bool:
"""Called when the widget needs to be redrawn.
area: The widget itself.
cr: The Cairo context to draw to. (We don't use it.)
Returns True to stop other handlers from being invoked
for the event, or False to propagate the event further.
"""
if not self.ctx: return False # something went wrong
self.ctx.clear(*self.bgColor)
query = self.ctx.query(samples=True, any_samples=False,
time=True, primitives=True)
with query:
self.lineDraw.run()
self.ctx.finish() # wait for finish, not really needed
err = self.ctx.error
if err is not None and err != "GL_NO_ERROR": print("GL ERROR:", err)
return True
class MyWindow(Gtk.Window):
"""Main application window."""
def __init__(self):
super().__init__()
self.connect('delete-event', Gtk.main_quit) # exit when window closed
self.glArea = MyGLArea()
self.add(self.glArea)
# trigger a redraw after the window is visible
def redraw():
self.queue_draw()
return True
GLib.timeout_add(500, redraw)
self.set_default_size(1473, 900) # arbitrary default size
self.show_all()
win = MyWindow()
Gtk.main() # won't return until quit event (ie window is closed)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.