Created
June 11, 2019 18:28
-
-
Save RenaKunisaki/2a1b184b55014067f04b44cf47b9fd55 to your computer and use it in GitHub Desktop.
moderngl vbo test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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