How to implement zoom towards mouse like in 3dsMax? - python

I'm trying to mimick the 3dsmax behaviour when you zoom in/out by moving the mouse wheel. In 3ds max this zooming will be towards the mouse position. So far I've come up with this little mcve:
import math
from ctypes import c_void_p
import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from glm import *
class Camera():
def __init__(
self,
eye=None, target=None, up=None,
fov=None, near=0.1, far=100000,
**kwargs
):
self.eye = vec3(eye) or vec3(0, 0, 1)
self.target = vec3(target) or vec3(0, 0, 0)
self.up = vec3(up) or vec3(0, 1, 0)
self.original_up = vec3(self.up)
self.fov = fov or radians(45)
self.near = near
self.far = far
def update(self, aspect):
self.view = lookAt(self.eye, self.target, self.up)
self.projection = perspective(self.fov, aspect, self.near, self.far)
def zoom(self, *args):
delta = -args[1] * 0.1
distance = length(self.target - self.eye)
self.eye = self.target + (self.eye - self.target) * (delta + 1)
def zoom_towards_cursor(self, *args):
x = args[2]
y = args[3]
v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
height = viewport.z
p0 = vec3(x, height - y, 0.0)
p1 = vec3(x, height - y, 1.0)
v1 = unProject(p0, self.view, self.projection, viewport)
v2 = unProject(p1, self.view, self.projection, viewport)
world_from = vec3(
(-v1.z * (v2.x - v1.x)) / (v2.z - v1.z) + v1.x,
(-v1.z * (v2.y - v1.y)) / (v2.z - v1.z) + v1.y,
0.0
)
self.eye.z = self.eye.z * (1.0 + 0.1 * args[1])
view = lookAt(self.eye, self.target, self.up)
v1 = unProject(p0, view, self.projection, viewport)
v2 = unProject(p1, view, self.projection, viewport)
world_to = vec3(
(v1.z * (v2.x - v1.x)) / (v2.z - v1.z) + v1.x,
(-v1.z * (v2.y - v1.y)) / (v2.z - v1.z) + v1.y,
0.0
)
offset = world_to - world_from
print(self.eye.z, world_from, world_to, offset)
self.eye += offset
self.target += offset
class GlutController():
def __init__(self, camera):
self.camera = camera
self.zoom = self.camera.zoom
def glut_mouse_wheel(self, *args):
self.zoom(*args)
class MyWindow:
def __init__(self, w, h):
self.width = w
self.height = h
glutInit()
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
glutInitWindowSize(w, h)
glutCreateWindow('OpenGL Window')
self.startup()
glutReshapeFunc(self.reshape)
glutDisplayFunc(self.display)
glutMouseWheelFunc(self.controller.glut_mouse_wheel)
glutKeyboardFunc(self.keyboard_func)
glutIdleFunc(self.idle_func)
def keyboard_func(self, *args):
try:
key = args[0].decode("utf8")
if key == "\x1b":
glutLeaveMainLoop()
if key in ['1']:
self.controller.zoom = self.camera.zoom
print("Using normal zoom")
elif key in ['2']:
self.controller.zoom = self.camera.zoom_towards_cursor
print("Using zoom towards mouse")
except Exception as e:
import traceback
traceback.print_exc()
def startup(self):
glEnable(GL_DEPTH_TEST)
aspect = self.width / self.height
params = {
"eye": vec3(10, 10, 10),
"target": vec3(0, 0, 0),
"up": vec3(0, 1, 0)
}
self.cameras = [
Camera(**params)
]
self.camera = self.cameras[0]
self.model = mat4(1)
self.controller = GlutController(self.camera)
def run(self):
glutMainLoop()
def idle_func(self):
glutPostRedisplay()
def reshape(self, w, h):
glViewport(0, 0, w, h)
self.width = w
self.height = h
def display(self):
self.camera.update(self.width / self.height)
glClearColor(0.2, 0.3, 0.3, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(degrees(self.camera.fov), self.width / self.height, self.camera.near, self.camera.far)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
e = self.camera.eye
t = self.camera.target
u = self.camera.up
gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)
glColor3f(1, 1, 1)
glBegin(GL_LINES)
for i in range(-5, 6):
if i == 0:
continue
glVertex3f(-5, 0, i)
glVertex3f(5, 0, i)
glVertex3f(i, 0, -5)
glVertex3f(i, 0, 5)
glEnd()
glBegin(GL_LINES)
glColor3f(1, 1, 1)
glVertex3f(-5, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, -5)
glVertex3f(0, 0, 0)
glColor3f(1, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(5, 0, 0)
glColor3f(0, 1, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 5, 0)
glColor3f(0, 0, 1)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, 5)
glEnd()
glutSwapBuffers()
if __name__ == '__main__':
window = MyWindow(800, 600)
window.run()
In this snippet you can switch between 2 zooming modes by pressing keys '1' or '2' keys.
When pressing '1' I'm doing an standard zooming, so far so good.
Problem is when pressing '2', in this case I've tried to adapt code from this thread to python/pyopengl/pygml but because I didn't understand very well the underlying maths of that answer I don't know very well how to fix the bad behaviour.
How would you fix the posted code so it will zoom in/out towards the mouse properly like 3dsmax?

A possible solution is to move the camera along a ray, from the camera position through the cursor (mouse) position and to move the target position in parallel.
self.eye = self.eye + ray_cursor * delta
self.target = self.target + ray_cursor * delta
For this the window position of the cursor has to be "un-projected" (unProject).
Calculate the cursor position in world space (e.g. on the far plane):
pt_wnd = vec3(x, height - y, 1.0)
pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
The ray from the eye position through the cursor is given by the the normalized vector from the eye position to the world space cursor position:
ray_cursor = normalize(pt_world - self.eye)
There is an issue in your code when you get the window height from the viewport rectangle, because the height is the .w component rather than the .z component:
v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
width = viewport.z
height = viewport.w
Full code listing of the function zoom_towards_cursor:
def zoom_towards_cursor(self, *args):
x = args[2]
y = args[3]
v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
width = viewport.z
height = viewport.w
pt_wnd = vec3(x, height - y, 1.0)
pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
ray_cursor = normalize(pt_world - self.eye)
delta = -args[1]
self.eye = self.eye + ray_cursor * delta
self.target = self.target + ray_cursor * delta
See also Python OpenGL 4.6, GLM navigation
Preview:

Related

Is there any way of cyclic rotation of 2-D lines in opengl

I want to make cyclic rotation of these 2-D lines such that when mouse is scrolled the lines move in upward or downward direction. For ex:- if mouse wheel is scrolled upward line at the top moves to the bottom and all lines move upward.
I am able to do this by changing data but it is a slow process.
Is there any way to do this using shader in OpenGl.
code:-
import OpenGL.GL as gl
import OpenGL.arrays.vbo as glvbo
from PyQt5.Qt import *
import numpy as np
import sys
import copy
VS1 = '''
#version 450
layout(location = 0) in vec2 position;
uniform float right;
uniform float bottom;
uniform float left;
uniform float top;
void main() {
const float far = 1.0;
const float near = -1.0;
mat4 testmat = mat4(
vec4(2.0 / (right - left), 0, 0, 0),
vec4(0, 2.0 / (top - bottom), 0, 0),
vec4(0, 0, -2.0 / (far - near), 0),
vec4(-(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1)
);
gl_Position = testmat * vec4(position.x, position.y, 0., 1.);
}
'''
FS1 = '''
#version 450
// Output variable of the fragment shader, which is a 4D vector containing the
// RGBA components of the pixel color.
uniform vec3 triangleColor;
out vec4 outColor;
void main()
{
outColor = vec4(triangleColor, 1.0);
}
'''
def compile_vertex_shader(source):
"""Compile a vertex shader from source."""
vertex_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex_shader, source)
gl.glCompileShader(vertex_shader)
# check compilation error
result = gl.glGetShaderiv(vertex_shader, gl.GL_COMPILE_STATUS)
if not (result):
raise RuntimeError(gl.glGetShaderInfoLog(vertex_shader))
return vertex_shader
def compile_fragment_shader(source):
"""Compile a fragment shader from source."""
fragment_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment_shader, source)
gl.glCompileShader(fragment_shader)
result = gl.glGetShaderiv(fragment_shader, gl.GL_COMPILE_STATUS)
if not (result):
raise RuntimeError(gl.glGetShaderInfoLog(fragment_shader))
return fragment_shader
def link_shader_program(vertex_shader, fragment_shader):
"""Create a shader program with from compiled shaders."""
program = gl.glCreateProgram()
gl.glAttachShader(program, vertex_shader)
gl.glAttachShader(program, fragment_shader)
gl.glLinkProgram(program)
result = gl.glGetProgramiv(program, gl.GL_LINK_STATUS)
if not (result):
raise RuntimeError(gl.glGetProgramInfoLog(program))
return program
class GLPlotWidget(QGLWidget):
def __init__(self, *args):
super(GLPlotWidget, self).__init__()
self.width, self.height = 100, 100
self.we = np.load('two.npy', mmap_mode='r')
self.e = copy.deepcopy(self.we[:, :, :])
self.w = copy.deepcopy(self.we[:, :, :])
for i in range(0, 24):
self.w[i, :, 1] = self.e[i, :, 1] - np.array(9999 * i)
# self.e[:, :, 1] = np.interp(self.e[:, :, 1], (self.e[:, :, 1].min(), self.e[:, :, 1].max()),
# (-1, 1))
#
# self.e[:, :, 0] = np.interp(self.e[:, :, 0], (self.e[:, :, 0].min(), self.e[:, :, 0].max()),
# (-1, +1))
self.right, self.left, self.top, self.bottom = self.e[0, -1, 0], self.e[
0, 0, 0], self.e[0, :, 1].max(), self.e[-1, :, 1].min()
self.vbo = glvbo.VBO(self.e)
self.count = self.vbo.shape[1]
self.scroll = 0
self.number_of_arm = 24
self.sensor_list_const = np.array(range(0, self.number_of_arm))
self.sensor_list_roll = copy.deepcopy(self.sensor_list_const)
self.showMaximized()
def initializeGL(self):
vs = compile_vertex_shader(VS1)
fs = compile_fragment_shader(FS1)
self.shaders_program_plot = link_shader_program(vs, fs)
def ortho_view(self, i):
right = gl.glGetUniformLocation(i, "right")
gl.glUniform1f(right, self.right)
left = gl.glGetUniformLocation(i, "left")
gl.glUniform1f(left, self.left)
top = gl.glGetUniformLocation(i, "top")
gl.glUniform1f(top, self.top)
bottom = gl.glGetUniformLocation(i, "bottom")
gl.glUniform1f(bottom, self.bottom)
def paintGL(self):
self.resizeGL(self.width, self.height)
gl.glClearColor(0.75, 0.75, 0.75, 0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
gl.glEnable(gl.GL_DEPTH_TEST)
self.vbo.bind()
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
gl.glUseProgram(self.shaders_program_plot)
self.ortho_view(self.shaders_program_plot)
uni_color = gl.glGetUniformLocation(self.shaders_program_plot, "triangleColor")
for i in range(0, self.vbo.data.shape[0]):
gl.glUniform3f(uni_color, 0, 0, 0)
gl.glLineWidth(1)
gl.glDrawArrays(gl.GL_LINE_STRIP, i * self.count, self.count)
self.vbo.unbind()
# self.greyscale()
# gl.glUseProgram(0)
def reset_vbo(self):
self.wex = copy.deepcopy(self.w)
for i, j in zip(self.sensor_list_roll, self.sensor_list_const):
self.wex[j, :, 1] = self.w[j, :, 1] + np.array(9999 * (self.number_of_arm - i))
self.vbo.set_array(self.wex)
self.right, self.left, self.top, self.bottom = self.wex[0, -1, 0], self.wex[
0, 0, 0], self.wex[:, :, 1].max(), self.wex[:, :, 1].min()
self.update()
def resizeGL(self, width, height):
self.width, self.height = width, height
gl.glViewport(0, 0, width, height)
def wheelEvent(self, *args, **kwargs):
event = args[0]
# print(event.angleDelta().y())
if event.angleDelta().y() > 0:
self.scroll = self.scroll - 1
else:
self.scroll = self.scroll + 1
if self.scroll > self.number_of_arm - 1 or self.scroll < -(self.number_of_arm - 1):
self.scroll = 0
self.sensor_list_roll = np.roll(self.sensor_list_const, self.scroll)
# self.patch_move(event)
self.reset_vbo()
def main():
app = QApplication(sys.argv)
editor = GLPlotWidget()
editor.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
data file :- https://drive.google.com/file/d/1y6w35kuMguR1YczK7yMJpXU86T6qtGSv/view?usp=sharing
You can use the uniforms top and bottom to scroll the view. You have to set the individually for each line.
Compute the scroll factor self.scroll dependent on the y scale of the projection:
class GLPlotWidget(QGLWidget):
# [...]
def wheelEvent(self, *args, **kwargs):
event = args[0]
scroll_scale = 0.01
size = self.top - self.bottom
if event.angleDelta().y() > 0:
self.scroll = self.scroll - size * scroll_scale
if self.scroll < 0:
self.scroll += size
else:
self.scroll = self.scroll + size * scroll_scale
if self.scroll > size:
self.scroll -= size
Further more you have to now the y range of each line. Compute the minimum and maximum for each line and store it to a list (self.linerange):
class GLPlotWidget(QGLWidget):
def __init__(self, *args):
# [...]
self.linerange = [(self.e[li, :, 1].max(), self.e[-li, :, 1].min()) for li in range(self.vbo.shape[0])]
Shift self.top and self.bottom by self.scroll for each line:
top, bottom = self.top+self.scroll, self.bottom+self.scroll
If the line would "fall out" of the window at the bottom, then lift it to the top of the view:
if self.linerange[i][0]-self.scroll < self.bottom:
top, bottom = top-size, bottom-size
Complete paintGL method:
class GLPlotWidget(QGLWidget):
# [...]
def paintGL(self):
self.resizeGL(self.width, self.height)
gl.glClearColor(0.75, 0.75, 0.75, 0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
gl.glEnable(gl.GL_DEPTH_TEST)
self.vbo.bind()
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
gl.glUseProgram(self.shaders_program_plot)
self.ortho_view(self.shaders_program_plot)
uni_color = gl.glGetUniformLocation(self.shaders_program_plot, "triangleColor")
loc_top = gl.glGetUniformLocation(self.shaders_program_plot, "top")
loc_bottom = gl.glGetUniformLocation(self.shaders_program_plot, "bottom")
for i in range(0, self.vbo.data.shape[0]):
size = self.top - self.bottom
top, bottom = self.top+self.scroll, self.bottom+self.scroll
if self.linerange[i][0]-self.scroll < self.bottom:
top, bottom = top-size, bottom-size
gl.glUniform1f(loc_top, top)
gl.glUniform1f(loc_bottom, bottom)
gl.glUniform3f(uni_color, 0, 0, 0)
gl.glLineWidth(1)
gl.glDrawArrays(gl.GL_LINE_STRIP, i * self.count, self.count)
self.vbo.unbind()

Best method to rotate triangles such that there is no gap between top and bottom line triangles after a full rotation?

I have created these triangles to form the greyscale surface of 2-D line plot. Now when I am rotating it using mouse wheel event, there is a gap between the top and bottom line and I want to remove it. Also the program run very slow after using this for loop in greyscale function. Can anyone suggest me any method or way to optimize this and make it work correctly?
code:-
import OpenGL.GL as gl
import OpenGL.arrays.vbo as glvbo
from PyQt5.Qt import *
import numpy as np
import sys
import copy
VS1 = '''
#version 450
layout(location = 0) in vec2 position;
uniform float right;
uniform float bottom;
uniform float left;
uniform float top;
void main() {
const float far = 1.0;
const float near = -1.0;
mat4 testmat = mat4(
vec4(2.0 / (right - left), 0, 0, 0),
vec4(0, 2.0 / (top - bottom), 0, 0),
vec4(0, 0, -2.0 / (far - near), 0),
vec4(-(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1)
);
gl_Position = testmat * vec4(position.x, position.y, 0., 1.);
}
'''
FS1 = '''
#version 450
// Output variable of the fragment shader, which is a 4D vector containing the
// RGBA components of the pixel color.
uniform vec3 triangleColor;
out vec4 outColor;
void main()
{
outColor = vec4(triangleColor, 1.0);
}
'''
VS = '''
#version 450
attribute vec2 position;
attribute vec3 a_Color;
uniform float right;
uniform float bottom;
uniform float left;
uniform float top;
out vec3 g_color;
void main() {
const float far = 1.0;
const float near = -1.0;
mat4 testmat = mat4(
vec4(2.0 / (right - left), 0, 0, 0),
vec4(0, 2.0 / (top - bottom), 0, 0),
vec4(0, 0, -2.0 / (far - near), 0),
vec4(-(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1)
);
gl_Position = testmat * vec4(position.x, position.y, 0., 1.);
g_color = a_Color;
}
'''
FS = '''
#version 450
// Output variable of the fragment shader, which is a 4D vector containing the
// RGBA components of the pixel color.
in vec3 g_color;
out vec4 outColor;
void main()
{
outColor = vec4(g_color, 1.0);
}
'''
def compile_vertex_shader(source):
"""Compile a vertex shader from source."""
vertex_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex_shader, source)
gl.glCompileShader(vertex_shader)
# check compilation error
result = gl.glGetShaderiv(vertex_shader, gl.GL_COMPILE_STATUS)
if not (result):
raise RuntimeError(gl.glGetShaderInfoLog(vertex_shader))
return vertex_shader
def compile_fragment_shader(source):
"""Compile a fragment shader from source."""
fragment_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment_shader, source)
gl.glCompileShader(fragment_shader)
result = gl.glGetShaderiv(fragment_shader, gl.GL_COMPILE_STATUS)
if not (result):
raise RuntimeError(gl.glGetShaderInfoLog(fragment_shader))
return fragment_shader
def link_shader_program(vertex_shader, fragment_shader):
"""Create a shader program with from compiled shaders."""
program = gl.glCreateProgram()
gl.glAttachShader(program, vertex_shader)
gl.glAttachShader(program, fragment_shader)
gl.glLinkProgram(program)
result = gl.glGetProgramiv(program, gl.GL_LINK_STATUS)
if not (result):
raise RuntimeError(gl.glGetProgramInfoLog(program))
return program
class GLPlotWidget(QGLWidget):
def __init__(self, *args):
super(GLPlotWidget, self).__init__()
self.width, self.height = 100, 100
self.we = np.load('two.npy', mmap_mode='r')
self.e = copy.deepcopy(self.we[:, :, :])
self.right, self.left, self.top, self.bottom = self.e[0, -1, 0], self.e[
0, 0, 0], self.e[0, :, 1].max(), self.e[-1, :, 1].min()
self.vbo = glvbo.VBO(self.e)
self.count = self.vbo.shape[1]
self.scroll = 0
self.number_of_arm = 24
self.linerange = [(self.e[li, :, 1].max(), self.e[-li, :, 1].min()) for li in range(self.vbo.shape[0])]
self.showMaximized()
def initializeGL(self):
vs = compile_vertex_shader(VS1)
fs = compile_fragment_shader(FS1)
self.shaders_program_plot = link_shader_program(vs, fs)
self.greyscale_data()
def greyscale_data(self):
self.color = np.zeros((self.e.shape[1] * (self.e.shape[0]), 3), dtype=np.float32)
for i in range(0, 24):
a = self.e[i, :, 1].min()
b = self.e[i, :, 1].max()
c = np.interp(self.e[i, :, 1], (a, b), (0.15, 0.85))
self.color[self.e.shape[1] * i:self.e.shape[1] * (i + 1), 0] = c
self.color[self.e.shape[1] * i:self.e.shape[1] * (i + 1), 1] = c
self.color[self.e.shape[1] * i:self.e.shape[1] * (i + 1), 2] = c
self.elems = []
b = self.e.shape[1] # number of points per line
a = self.e.shape[0] # total number of arms
for i in range(0, a):
if i < a-1:
for j in range(0, b - 1):
self.elems += [j + b * i, j + b * i + 1, j + b * (i + 1)]
self.elems += [j + b * (i + 1), j + b * (i + 1) + 1, j + b * i + 1]
else:
for j in range(0, b - 1):
self.elems += [j + b * i, j + b * i + 1, j]
self.elems += [j, j + 1, j + b * i + 1]
self.elems = np.array(self.elems, dtype=np.int32)
# print(self.elems[0:100])
vs = compile_vertex_shader(VS)
fs = compile_fragment_shader(FS)
self.shaders_program = link_shader_program(vs, fs)
self.vertexbuffer = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexbuffer)
gl.glBufferData(gl.GL_ARRAY_BUFFER, self.e, gl.GL_DYNAMIC_DRAW)
self.elementbuffer = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.elementbuffer)
gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, self.elems, gl.GL_DYNAMIC_DRAW)
self.colorbuffer = gl.glGenBuffers(1)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.colorbuffer)
gl.glBufferData(gl.GL_ARRAY_BUFFER, self.color, gl.GL_DYNAMIC_DRAW)
def ortho_view(self, i):
right = gl.glGetUniformLocation(i, "right")
gl.glUniform1f(right, self.right)
left = gl.glGetUniformLocation(i, "left")
gl.glUniform1f(left, self.left)
top = gl.glGetUniformLocation(i, "top")
gl.glUniform1f(top, self.top)
bottom = gl.glGetUniformLocation(i, "bottom")
gl.glUniform1f(bottom, self.bottom)
def greyscale(self):
gl.glUseProgram(self.shaders_program)
self.ortho_view(self.shaders_program)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexbuffer)
stride = 0 # 3*self.e.itemsize
offset = None # ctypes.c_void_p(0)
loc = gl.glGetAttribLocation(self.shaders_program, 'position')
gl.glEnableVertexAttribArray(loc)
gl.glVertexAttribPointer(loc, 2, gl.GL_FLOAT, False, stride, offset)
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.elementbuffer)
loc = gl.glGetAttribLocation(self.shaders_program, 'a_Color')
gl.glEnableVertexAttribArray(loc)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.colorbuffer)
gl.glVertexAttribPointer(loc, 3, gl.GL_FLOAT, False, stride, offset)
loc_top1 = gl.glGetUniformLocation(self.shaders_program, "top")
loc_bottom1 = gl.glGetUniformLocation(self.shaders_program, "bottom")
for i in range(0, 24):
size = self.top - self.bottom
top, bottom = self.top + self.scroll, self.bottom + self.scroll
if self.linerange[i][0] - self.scroll < self.bottom:
top, bottom = top - size, bottom - size
gl.glUniform1f(loc_top1, top)
gl.glUniform1f(loc_bottom1, bottom)
a = int(i * self.elems.size)
b = int((i+1) * self.elems.size)
c = int(self.elems.size/24)
# gl.glDrawElements(gl.GL_TRIANGLE_STRIP, self.elems.size, gl.GL_UNSIGNED_INT, None)
gl.glDrawRangeElements(gl.GL_TRIANGLE_STRIP, a, b, self.elems.size, gl.GL_UNSIGNED_INT, None)
def paintGL(self):
self.resizeGL(self.width, self.height)
gl.glClearColor(1, 1, 1, 0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
gl.glEnable(gl.GL_DEPTH_TEST)
self.vbo.bind()
gl.glEnableVertexAttribArray(0)
gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
gl.glUseProgram(self.shaders_program_plot)
self.ortho_view(self.shaders_program_plot)
uni_color = gl.glGetUniformLocation(self.shaders_program_plot, "triangleColor")
loc_top = gl.glGetUniformLocation(self.shaders_program_plot, "top")
loc_bottom = gl.glGetUniformLocation(self.shaders_program_plot, "bottom")
for i in range(0, self.vbo.data.shape[0]):
size = self.top - self.bottom
top, bottom = self.top + self.scroll, self.bottom + self.scroll
if self.linerange[i][0] - self.scroll < self.bottom:
top, bottom = top - size, bottom - size
gl.glUniform1f(loc_top, top)
gl.glUniform1f(loc_bottom, bottom)
gl.glUniform3f(uni_color, 0, 0, 0)
gl.glLineWidth(1)
gl.glDrawArrays(gl.GL_LINE_STRIP, i * self.count, self.count)
self.vbo.unbind()
self.greyscale()
# gl.glUseProgram(0)
def resizeGL(self, width, height):
self.width, self.height = width, height
gl.glViewport(0, 0, width, height)
def wheelEvent(self, *args, **kwargs):
event = args[0]
scroll_scale = 0.01
size = self.top - self.bottom
if event.angleDelta().y() > 0:
self.scroll = self.scroll - size * scroll_scale
if self.scroll < 0:
self.scroll += size
else:
self.scroll = self.scroll + size * scroll_scale
if self.scroll > size:
self.scroll -= size
self.update()
def main():
app = QApplication(sys.argv)
editor = GLPlotWidget()
editor.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
data file:- https://drive.google.com/file/d/1y6w35kuMguR1YczK7yMJpXU86T6qtGSv/view?usp=sharing
Add a straight line at the at the bin and the end of the data.
Compute the minimum and the maximum of the original data, the y scale of the data and the average offset from one line to another:
origshape = self.e.shape[:]
origmin, origmax = self.e[0, :, 1].max(), self.e[-1, :, 1].min()
origsize = origmax - origmin
origoffset = origsize / origshape[0]
Compute a new minimum and maximum with an certain offset (origoffset/2) and add a straight line at the begin and the end. Copy the first and last line and change the y component of the new lines by newmax respectively newmin
newmin, newmax = origmin - origoffset/2, origmax + origoffset/2
self.first = self.e[0,:,:].copy().reshape((1, *origshape[1:]))
self.last = self.e[-1,:,:].copy().reshape((1, *origshape[1:]))
self.first[:,:,1] = newmax
self.last[:,:,1] = newmin
self.e = np.concatenate((self.first, self.e, self.last))
New constructor of GLPlotWidget:
class GLPlotWidget(QGLWidget):
def __init__(self, *args):
super(GLPlotWidget, self).__init__()
self.width, self.height = 100, 100
self.we = np.load('two.npy', mmap_mode='r')
self.e = copy.deepcopy(self.we[:, :, :])
origshape = self.e.shape[:]
origmin, origmax = self.e[-1, :, 1].min(), self.e[1, :, 1].max()
origsize = origmax - origmin
origoffset = origsize / origshape[0]
newmin, newmax = origmin - origoffset/2, origmax + origoffset/2
self.first = self.e[0,:,:].copy().reshape((1, *origshape[1:]))
self.last = self.e[-1,:,:].copy().reshape((1, *origshape[1:]))
self.first[:,:,1] = newmax
self.last[:,:,1] = newmin
self.e = np.concatenate((self.first, self.e, self.last))
self.right, self.left, self.top, self.bottom = self.e[0, -1, 0], self.e[
0, 0, 0], self.e[0, :, 1].max(), self.e[-1, :, 1].min()
self.vbo = glvbo.VBO(self.e)
self.count = self.vbo.shape[1]
self.scroll = 0
self.number_of_arm = 24
self.linerange = [(self.e[li, :, 1].max(), self.e[-li, :, 1].min()) for li in range(self.vbo.shape[0])]
self.showMaximized()

Tile rendering with opengl

Let's start by considering this simple snippet:
import ctypes
import textwrap
import time
import glfw
import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import glm
GLSL_VERSION = "#version 440\n"
CONTEXT_VERSION = (4, 1)
def vs_shader(text):
return GLSL_VERSION + textwrap.dedent(text)
def shader(text):
prefix = textwrap.dedent("""\
uniform float iTime;
uniform int iFrame;
uniform vec3 iResolution;
uniform sampler2D iChannel0;
uniform vec2 iOffset;
out vec4 frag_color;
""")
suffix = textwrap.dedent("""\
void main() {
mainImage(frag_color, gl_FragCoord.xy + iOffset);
}
""")
return GLSL_VERSION + prefix + textwrap.dedent(text) + suffix
VS = vs_shader("""\
layout(location = 0) in vec3 in_position;
uniform mat4 mvp;
void main()
{
gl_Position = mvp * vec4(in_position, 1.0f);
}
""")
SIMPLE = [
shader("""
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
float tile_size = 4;
vec2 g = floor(vec2(tile_size, tile_size) * uv);
float c = mod(g.x + g.y, 2.0);
if (uv.x<0.5 && uv.y<0.5)
fragColor = vec4(mix(vec3(c), vec3(1), vec3(1,0,1)), 1.0);
else if (uv.x>=0.5 && uv.y<0.5)
fragColor = vec4(mix(vec3(c), vec3(1), vec3(1,0,0)), 1.0);
else if (uv.x<0.5 && uv.y>=0.5)
fragColor = vec4(mix(vec3(c), vec3(1), vec3(0,1,0)), 1.0);
else if (uv.x>=0.5 && uv.y>=0.5)
fragColor = vec4(mix(vec3(c), vec3(1), vec3(0,0,1)), 1.0);
}
"""),
shader("""
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
fragColor = vec4(texture(iChannel0, uv).rgb,1.0);
}
""")
]
# -------- MINIFRAMEWORK --------
class Tiler:
def __init__(self, scene_width, scene_height):
self.scene_width = scene_width
self.scene_height = scene_height
#classmethod
def from_num_tiles(cls, scene_width, scene_height, num_tiles_x, num_tiles_y):
obj = cls(scene_width, scene_height)
obj.num_tiles_x = num_tiles_x
obj.num_tiles_y = num_tiles_y
obj.tile_width = obj.scene_width // num_tiles_x
obj.tile_height = obj.scene_height // num_tiles_y
return obj
#classmethod
def from_size(cls, scene_width, scene_height, tile_width, tile_height):
obj = cls(scene_width, scene_height)
obj.num_tiles_x = obj.scene_width // tile_width
obj.num_tiles_y = obj.scene_height // tile_height
obj.tile_width = tile_width
obj.tile_height = tile_height
return obj
#property
def num_tiles(self):
return self.num_tiles_y * self.num_tiles_x
class TextureF32():
def __init__(self, width, height):
target = GL_TEXTURE_2D
self.target = target
self.identifier = glGenTextures(1)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
glBindTexture(target, self.identifier)
glTexImage2D(target, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, None)
glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
self.set_filter()
glBindTexture(target, 0)
def set_filter(self):
glTexParameteri(self.target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(self.target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
def bind(self):
glBindTexture(self.target, self.identifier)
def unbind(self):
glBindTexture(self.target, 0)
class FboF32():
def __init__(self, width, height):
self.target = GL_FRAMEBUFFER
self.identifier = glGenFramebuffers(1)
glBindFramebuffer(GL_FRAMEBUFFER, self.identifier)
# Color attachments
tex = TextureF32(width, height)
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex.identifier, 0)
glDrawBuffers(1, [GL_COLOR_ATTACHMENT0])
self.colors = [tex]
self.width = width
self.height = height
if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
raise Exception(
f"ERROR::FRAMEBUFFER:: Framebuffer {self.identifier} is not complete!"
)
glBindFramebuffer(GL_FRAMEBUFFER, 0)
def delete(self):
self.glDeleteFramebuffers(self.identifier)
def rect(self):
return [0, 0, self.width, self.height]
def bind(self):
glBindFramebuffer(GL_FRAMEBUFFER, self.identifier)
def set_uniform1f(prog, name, v0):
glUniform1f(glGetUniformLocation(prog, name), v0)
def set_uniform1i(prog, name, v0):
glUniform1i(glGetUniformLocation(prog, name), v0)
def set_uniform2i(prog, name, v0, v1):
glUniform2i(glGetUniformLocation(prog, name), v0, v1)
def set_uniform2f(prog, name, v0, v1):
glUniform2f(glGetUniformLocation(prog, name), v0, v1)
def set_uniform3f(prog, name, v0, v1, v2):
glUniform3f(glGetUniformLocation(prog, name), v0, v1, v2)
def set_uniform_mat4(prog, name, mat):
glUniformMatrix4fv(glGetUniformLocation(prog, name), 1, GL_FALSE, glm.value_ptr(mat))
def set_uniform_texture(prog, name, resource, unit_texture):
glActiveTexture(GL_TEXTURE0 + unit_texture)
resource.bind()
resource.set_filter()
glUniform1i(glGetUniformLocation(prog, name), 0 + unit_texture)
def create_quad(x0, y0, x1, y1):
data = np.array([
x0, y0, 0,
x1, y0, 0,
x0, y1, 0,
x1, y0, 0,
x1, y1, 0,
x0, y1, 0,
], dtype=np.float32)
vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glBufferData(GL_ARRAY_BUFFER, data, GL_STATIC_DRAW)
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
glVertexAttribPointer(0, 3, GL_FLOAT, False, 0, ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
return vao
def compile(shader_type, source):
identifier = glCreateShader(shader_type)
glShaderSource(identifier, source)
glCompileShader(identifier)
if not glGetShaderiv(identifier, GL_COMPILE_STATUS):
for i, l in enumerate(source.splitlines()):
print(f"{i+1}: {l}")
raise Exception(glGetShaderInfoLog(identifier).decode("utf-8"))
return identifier
def create_program(vs, fs):
vs_identifier = compile(GL_VERTEX_SHADER, vs)
fs_identifier = compile(GL_FRAGMENT_SHADER, fs)
program = glCreateProgram()
glAttachShader(program, vs_identifier)
glAttachShader(program, fs_identifier)
glLinkProgram(program)
if not glGetProgramiv(program, GL_LINK_STATUS):
raise RuntimeError(glGetProgramInfoLog(program))
return program
# -------- Glut/Glfw --------
class Effect:
def __init__(self, w, h, num_tiles_x, num_tiles_y, passes):
self.fbos = []
self.needs_updating = True
self.allocations = 0
self.tiler = Tiler.from_num_tiles(w, h, num_tiles_x, num_tiles_y)
self.passes = [create_program(VS, rp) for rp in passes]
self.iframe = 0
self.start_time = time.time()
self.quad = create_quad(-1, -1, 1, 1)
self.view = glm.lookAt(
glm.vec3(0, 0, 10),
glm.vec3(0, 0, 0),
glm.vec3(0, 1, 0)
)
self.model = glm.mat4(1)
glEnable(GL_DEPTH_TEST)
# print("GL_MAX_VIEWPORT_DIMS:", glGetIntegerv(GL_MAX_VIEWPORT_DIMS))
# print("GL_MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE))
# print("GL_MAX_RENDERBUFFER_SIZE:", glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE))
def mem_info(self):
GL_GPU_MEM_INFO_TOTAL_AVAILABLE_MEM_NVX = 0x9048
GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX = 0x9049
total_mem_kb = glGetIntegerv(GL_GPU_MEM_INFO_TOTAL_AVAILABLE_MEM_NVX)
cur_avail_mem_kb = glGetIntegerv(GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX)
return f"total_mem_kb={total_mem_kb} cur_avail_mem_kb={cur_avail_mem_kb}"
def create_fbo(self, tiler):
return [
FboF32(width=tiler.tile_width, height=tiler.tile_height)
for i in range(tiler.num_tiles)
]
def make_ortho(self, x, y, num_tiles_x, num_tiles_y, left, right, bottom, top, near, far):
# References
#
# https://www.opengl.org/archives/resources/code/samples/advanced/advanced97/notes/node20.html
# https://stackoverflow.com/questions/6490728/capture-snapshot-of-opengl-window-with-very-large-image-resolution
#
offset_x = (right - left) / num_tiles_x
offset_y = (top - bottom) / num_tiles_y
l = left + offset_x * x
r = left + offset_x * (x + 1)
b = bottom + offset_y * y
t = bottom + offset_y * (y + 1)
n = near
f = far
print(f"x={x} y={y} left={l} right={r} bottom={b} top={t}")
return glm.ortho(l, r, b, t, n, f)
def render_pass(self, rp, mvp, w, h, channel0, offset_x=0, offset_y=0):
t = time.time() - self.start_time
glBindVertexArray(self.quad)
glUseProgram(rp)
set_uniform_mat4(rp, "mvp", mvp)
set_uniform1f(rp, "iTime", t)
set_uniform1i(rp, "iFrame", self.iframe)
set_uniform3f(rp, "iResolution", w, h, w / h)
set_uniform2f(rp, "iOffset", offset_x, offset_y)
if channel0:
set_uniform_texture(rp, "iChannel0", channel0, self.active_texture)
self.active_texture += 1
glDrawArrays(GL_TRIANGLES, 0, 6)
# No tile rendering
def render_no_tiles(self, window_width, window_height):
self.active_texture = 0
if self.needs_updating:
if not self.fbos:
print(f"Creating fbos, allocations={self.allocations} {self.mem_info()}")
self.fbos = [
FboF32(width=window_width, height=window_height),
FboF32(width=window_width, height=window_height)
]
# clear buffers
if self.iframe == 0:
for fbo in self.fbos:
fbo.bind()
glViewport(*fbo.rect())
glClearColor(0, 0, 0, 0)
glClear(GL_COLOR_BUFFER_BIT)
proj = glm.ortho(-1, 1, -1, 1, -100, 100)
mvp = proj * self.view * self.model
# Pass0: BufferA - Channels [BufferA, None, None, None]
fbo0 = self.fbos[0]
fbo1 = self.fbos[1]
w, h = fbo0.width, fbo0.height
rp = self.passes[0]
fbo0.bind()
glViewport(0, 0, w, h)
self.render_pass(rp, mvp, w, h, fbo1.colors[0])
# Pass1: Image - Channels [BufferA, None, None, None]
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
fbo0 = self.fbos[0]
w, h = window_width, window_height
rp = self.passes[1]
glViewport(0, 0, w, h)
self.render_pass(rp, mvp, w, h, fbo0.colors[0])
# ping-pong
self.fbos.reverse()
self.iframe += 1
# Tile rendering
def render_tiles(self, window_width, window_height):
M = self.tiler.num_tiles_x
N = self.tiler.num_tiles_y
offset_x = window_width // M
offset_y = window_height // N
proj = glm.ortho(-1, 1, -1, 1, -100, 100)
# -------- Test --------
# glBindFramebuffer(GL_FRAMEBUFFER, 0)
# glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# self.active_texture = 0
# for y in range(N):
# for x in range(M):
# w, h = window_width, window_height
# mvp = proj * self.view * self.model
# glViewport(offset_x * x, offset_y * y, self.tiler.tile_width, self.tiler.tile_height)
# self.render_pass(self.passes[0], mvp, w, h, None, offset_x * x, offset_y * y)
# return
# -------- Test2 --------
self.active_texture = 0
if self.needs_updating:
if not self.fbos:
print(f"Creating fbos, allocations={self.allocations} {self.mem_info()}")
self.fbos = [
self.create_fbo(self.tiler),
self.create_fbo(self.tiler),
]
# clear buffers
if self.iframe == 0:
for fbo_tiles in self.fbos:
for fbo in fbo_tiles:
fbo.bind()
glViewport(*fbo.rect())
glClearColor(0, 0, 0, 0)
glClear(GL_COLOR_BUFFER_BIT)
# Pass0: BufferA - Channels [BufferA, None, None, None]
for y in range(N):
for x in range(M):
fbo0 = self.fbos[0][y * M + x]
fbo1 = self.fbos[1][y * M + x]
w, h, aspect = fbo0.width, fbo0.height, fbo0.width / fbo0.height
mvp = proj * self.view * self.model
rp = self.passes[0]
fbo0.bind()
glViewport(0, 0, self.tiler.tile_width, self.tiler.tile_height)
self.render_pass(rp, mvp, w, h, fbo1.colors[0], offset_x * x, offset_y * y)
# Pass1: Image - Channels [BufferA, None, None, None]
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
for y in range(N):
for x in range(M):
fbo0 = self.fbos[0][y * M + x]
fbo1 = self.fbos[1][y * M + x]
w, h, aspect = window_width, window_height, window_width / window_height
mvp = proj * self.view * self.model
rp = self.passes[1]
glViewport(offset_x * x, offset_y * y, self.tiler.tile_width, self.tiler.tile_height)
self.render_pass(rp, mvp, w, h, fbo0.colors[0], 0, 0)
# ping-pong
self.fbos.reverse()
self.iframe += 1
class WindowGlut:
def __init__(self, w, h, use_tiles, num_tiles_x, num_tiles_y, passes):
glutInit()
glutInitContextVersion(*CONTEXT_VERSION)
glutInitContextProfile(GLUT_CORE_PROFILE)
glutInitContextFlags(GLUT_FORWARD_COMPATIBLE)
glutSetOption(GLUT_MULTISAMPLE, 16)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_MULTISAMPLE)
glutInitWindowSize(w, h)
glutCreateWindow('Mcve')
glutReshapeFunc(self.reshape)
glutKeyboardFunc(self.keyboard_func)
glutKeyboardUpFunc(self.keyboard_up_func)
glutDisplayFunc(self.display)
glutIdleFunc(self.idle_func)
self.keys = {chr(i): False for i in range(256)}
self.effect = Effect(w, h, num_tiles_x, num_tiles_y, passes)
self.start_time = time.time()
self.num_frames = 0
if use_tiles:
print("TILE RENDERING ENABLED")
self.render = self.effect.render_tiles
else:
print("TILE RENDERING DISABLED")
self.render = self.effect.render_no_tiles
def keyboard_func(self, *args):
self.keys[args[0].decode("utf8")] = True
def keyboard_up_func(self, *args):
self.keys[args[0].decode("utf8")] = False
def display(self):
if self.keys['r']:
self.effect.iframe = 0
self.render(self.window_width, self.window_height)
glutSwapBuffers()
self.num_frames += 1
t = time.time() - self.start_time
if t >= 1:
glutSetWindowTitle(f"Fps: {self.num_frames}")
self.start_time = time.time()
self.num_frames = 0
def run(self):
glutMainLoop()
def idle_func(self):
glutPostRedisplay()
def reshape(self, w, h):
glViewport(0, 0, w, h)
self.window_width = w
self.window_height = h
class WindowGlfw:
def __init__(self, w, h, use_tiles, num_tiles_x, num_tiles_y, passes):
# Initialize the library
if not glfw.init():
return
# Create a windowed mode window and its OpenGL context
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, CONTEXT_VERSION[0])
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, CONTEXT_VERSION[1])
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, GL_TRUE)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
window = glfw.create_window(w, h, "Mcve", None, None)
if not window:
glfw.terminate()
return
glfw.set_window_size_callback(window, self.reshape)
glfw.set_key_callback(window, self.keyboard_func)
# Make the window's context current
glfw.make_context_current(window)
self.window = window
self.keys = {chr(i): False for i in range(256)}
self.effect = Effect(w, h, num_tiles_x, num_tiles_y, passes)
self.window_width = w
self.window_height = h
if use_tiles:
print("TILE RENDERING ENABLED")
self.render = self.effect.render_tiles
else:
print("TILE RENDERING DISABLED")
self.render = self.effect.render_no_tiles
def keyboard_func(self, window, key, scancode, action, mods):
self.keys[chr(key)] = action
def display(self):
if self.keys['R']:
self.iframe = 0
self.render(self.window_width, self.window_height)
def run(self):
window = self.window
while not glfw.window_should_close(window):
self.display()
glfw.swap_buffers(window)
glfw.poll_events()
glfw.terminate()
def reshape(self, window, w, h):
glViewport(0, 0, w, h)
self.window_width = w
self.window_height = h
if __name__ == '__main__':
params = {
"w": 320,
"h": 240,
"use_tiles": True,
"num_tiles_x": 2,
"num_tiles_y": 2,
"passes": SIMPLE
}
use_glut = True
WindowGlut(**params).run() if use_glut else WindowGlfw(**params).run()
To run this code you'll need to install numpy, pyopengl, glfw, PyGLM. You can switch between glfw or glut by toggling the variable use_glut. I've added this options as it seems running glut on macosx may be tricky in certain cases.
Anyway, the goal of this thread is to figure out how to fix the buggy snippet to make proper tile rendering, as you can see right now there is a very naive attempt implemented.
In the main block you can specify if you want to use a render method using tiles or not (use_tiles variable), if you choose using tiles you'll need to specify the number of them (num_tiles_x, num_tiles_y).
Cases:
If you run it with "use_tiles": False you'll see this output:
that output is correct
If you run it with "use_tiles": True, "num_tiles_x": 2, "num_tiles_y": 2 you should see the same output than 1). Also correct
But if you run it with "use_tiles": True, "num_tiles_x": 4, "num_tiles_y": 4 or higher you'll start seeing a totally screwed up image like below:
QUESTION: What's the bug of my tile rendering code that's producing the wrong output? How would you fix it?
Also... Even if the code is fixed the way I'm trying to make tile rendering is quite naive and it won't work very well when dealing with more complex effects where passes need to read back from adjacent tiles or even worst, non-adjacent tiles. For the case of adjacent tiles I've been told adding some padding to the tiles would work pretty well but for the more general case i don't have a clue how'd you tackle that problem. In any case, one step at a time, the goal of this thread would be fixing the buggy snippet
In the first pass a single tile is rendered to a framebuffer, which has exactly the size of the tile. gl_FragCoord.xy is (0,0) at the bottom left of the tile. uv = (0,0) has to be at the bottom left of the window and uv = (1, 1) at the top right of the window. To calculate the the uv coordinate in respect to the window, you've to add the offset of the tile to gl_FragCoord.xy and to divide by the size of the window:
formula (pseudo code):
uv = (gl_FragCoord.xy + (offset_x*x, offset_y*y)) / (window_width, window_height)
+------------------+
| |
| +----+ |
| | | |
| +----+ |
| (0,0) tile = gl_FragCoord.xy
| |
+------------------+
(0,0) window
In the first pass, iResolution has to be (window_width, window_height) and iOffset has to be (offset_x * x, offset_y * y).
# Pass0: BufferA - Channels [BufferA, None, None, None]
for y in range(N):
for x in range(M):
fbo0 = self.fbos[0][y * M + x]
fbo1 = self.fbos[1][y * M + x]
mvp = proj * self.view * self.model
rp = self.passes[0]
fbo0.bind()
glViewport(0, 0, self.tiler.tile_width, self.tiler.tile_height)
w, h = window_width, window_height
aspect = window_width / window_height
self.render_pass(rp, mvp, w, h, fbo1.colors[0], offset_x * x, offset_y * y)
In the second pass a single tile is read form the texture and renderd to the window (default framebuffer 0). The source texture (tile) has exactly the size of a tile and the uv coordinate has to be calculated in respect to the tile texture. gl_FragCoord.xy is (0,0) at the bottom left of the window. uv = (0,0) has to be at the bottom left of the tile and uv = (1, 1) at the top right of the tile. To calculate the uv coordinate the offset of the tile has to subtracted from gl_FragCoord.xy and the result has to be divided by the size of a title :
formula (pseudo code)
uv = (gl_FragCoord.xy - (offset_x*x, offset_y*y)) / (tile_width, tile_height)
+------------------+
| |
| +----+ |
| | | |
| +----+ |
| (0,0) tile |
| |
+------------------+
(0,0) window = gl_FragCoord.xy
In the 2nd pass, iResolution has to be (self.tiler.tile_width, self.tiler.tile_height) and iOffset has to be (-offset_x * x, -offset_y * y).
# Pass1: Image - Channels [BufferA, None, None, None]
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
for y in range(N):
for x in range(M):
fbo0 = self.fbos[0][y * M + x]
fbo1 = self.fbos[1][y * M + x]
mvp = proj * self.view * self.model
rp = self.passes[1]
glViewport(offset_x*x, offset_y*y, self.tiler.tile_width, self.tiler.tile_height)
w, h = self.tiler.tile_width, self.tiler.tile_height
aspect = self.tiler.tile_width / self.tiler.tile_height
self.render_pass(rp, mvp, w, h, fbo0.colors[0], -offset_x * x, -offset_y * y)
Edit for mcve.py
In this case the render target is always a framebuffer with the size of a tile The 2nd render pass ("Pass1") reads from a tile and stores to the destination tile, so the 2nd pass has to be:
# Pass1: Image - Channels [BufferA, None, None, None]
for y in range(N):
for x in range(M):
fbo_dst = self.fbo_target[0][y * M + x]
fbo_src = self.fbos[0][y * M + x]
mvp = proj * self.view * self.model
rp = self.passes[1]
fbo_dst.bind()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glViewport(0, 0, self.tiler.tile_width, self.tiler.tile_height)
w, h = self.tiler.tile_width, self.tiler.tile_height
aspect = self.tiler.tile_width / self.tiler.tile_height
self.render_pass(rp, mvp, w, h, fbo_src.colors[0], 0, 0)
A further issue is the reading of the texture form the previous frame in the fragments shader. The size of the texture is always a the size of a tile. The bottom left coordinate of the texture is (0, 0) and the top right coordinate is (1, 1).
So for the calculation of the texture coordinate (st), the offset has to be skipped and the resolution is given by the size of the texture (textureSize):
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
initSpheres();
# issue is here
// vec2 st = fragCoord.xy / iResolution.xy; <--- delete
vec2 st = gl_FragCoord.xy / vec2(textureSize(iChannel0, 0));
// [...]
// Moving average (multipass code)
vec3 color = texture(iChannel0, st).rgb * float(iFrame);
// [...]
}
See the result:
If you don't want to change the shader code in mainImage, then a different approach is to trick the system and to delegate the texture look up to a different function, by a macro. e.g.:
def shader(tileTextureLookup, text):
prefix = textwrap.dedent("""\
uniform float iTime;
uniform int iFrame;
uniform vec3 iResolution;
uniform sampler2D iChannel0;
uniform vec2 iOffset;
out vec4 frag_color;
""")
textureLookup = ""
if tileTextureLookup:
textureLookup = textwrap.dedent("""\
vec4 textureTile(sampler2D sampler, vec2 uv) {
vec2 st = (uv * iResolution.xy - iOffset.xy) / vec2(textureSize(sampler, 0));
return texture(sampler, st);
}
#define texture textureTile
""")
suffix = textwrap.dedent("""\
void main() {
mainImage(frag_color, gl_FragCoord.xy + iOffset);
}
""")
return GLSL_VERSION + prefix + textureLookup + textwrap.dedent(text) + suffix
SMALLPT_MULTIPASS = [
shader(True, """\
// All code here is by Zavie (https://www.shadertoy.com/view/4sfGDB#)
// [...]
"""),
shader(False, """\
// A simple port of Zavie's GLSL smallpt that uses multipass.
// Original source: https://www.shadertoy.com/view/4sfGDB#
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = fragCoord.xy / iResolution.xy;
vec3 color = texture(iChannel0, uv).rgb;
fragColor = vec4(pow(clamp(color, 0., 1.), vec3(1./2.2)), 1.);
}
""")
]
But note, texture is an overloaded function and this approach works for 2 dimensional textures only. Furthermore there other look up functions like texelFetch, too.

Kivy custom shaders touch events

Am using custom shader on my game, and i must because of performance. Now i am at the point where i want to bind touch events to my particles/Ufo so that i can decide what to do when someone touches them, but i don't know how i can calculate their width and height. I am currently able to tell where a touch event happened but my collide_point function always return False because i don't have the correct Width and Height of my game particle's. collide_point function require particle's right and top and particle's right and top requires particle's width and height to work. In the documentation it is said that
the width and the height property is subject to layout logic
but am not using any Layout, am using Widget instead. How can i calculate my game particle's width and height. Below is the code
from __future__ import division
from collections import namedtuple
import json
import math
import random
from kivy import platform
from kivy.app import App
from kivy.base import EventLoop
from kivy.clock import Clock
from kivy.core.image import Image
from kivy.core.window import Window
from kivy.graphics import Mesh
from kivy.graphics.instructions import RenderContext
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
import base64
UVMapping = namedtuple('UVMapping', 'u0 v0 u1 v1 su sv')
GLSL = """
---vertex
$HEADER$
attribute vec2 vCenter;
attribute float vScale;
void main(void)
{
tex_coord0 = vTexCoords0;
mat4 move_mat = mat4
(1.0, 0.0, 0.0, vCenter.x,
0.0, 1.0, 0.0, vCenter.y,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
vec4 pos = vec4(vPosition.xy * vScale, 0.0, 1.0)
* move_mat;
gl_Position = projection_mat * modelview_mat * pos;
}
---fragment
$HEADER$
void main(void)
{
gl_FragColor = texture2D(texture0, tex_coord0);
}
"""
with open("game.glsl", "wb") as glslc:
glslc.write(GLSL)
def load_atlas():
atlas = json.loads('''{"game-0.png": {"Elien": [2, 26, 100, 100]}}''')
tex_name, mapping = atlas.popitem()
data = '''iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAACJklEQVR4nO3dy1ICQRAF0YLw/39ZVxMBGCjEMF23JvOsXPgounMaN61VQrtsH3x3TqFfLv9/ykdcq9z8RKv25Lro5yiUAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAfXUPsNM79ydWXbYZZVoAey7MPH6tQdScAI64KbV9T3QI6QGsuCKHDiH5l8DVd1aRd2QTT4DOjcCdBmknQMpTmDLH4ZICSFv0tHkOkRJA6mKnzvUxCQGkL3L6fLskBKBG3QFMebqmzPm2zgCmLeq0eV/SfQKoWVcAU5+mqXM/5QkA1xHA9Kdo+vx3PAHgDADOAOBWB3CW98+zvA5PADoDgDMAOAOAMwA4A4AzADgDgDMAuNUBnOXCxVlehycAnQHAGQBcRwDT3z+nz3/HEwCuK4CpT9HUuZ/yBIDrDGDa0zRt3pd0nwBTFnXKnG/rDkDNEgJIf7rS59slIYCq3EVOnetjUgKoylvstHkOkRRAVc6ip8xxuMS/E7gtfsflC8zGb9JOgFurNwO3+VWZJ8CtFacBcuM36QFsjggBvfGbKQFsHjfNfxix07QAHrmpOyX/EqgFDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwA7lrl7YpE7okkSZIkSZIkSZIkSZIkSZIkSZL+9AMvSSThyPfOhQAAAABJRU5ErkJggg=='''
with open(tex_name, "wb") as co:
co.write(base64.b64decode(data))
tex = Image(tex_name).texture
tex_width, tex_height = tex.size
uvmap = {}
for name, val in mapping.items():
x0, y0, w, h = val
x1, y1 = x0 + w, y0 + h
uvmap[name] = UVMapping(
x0 / tex_width, 1 - y1 / tex_height,
x1 / tex_width, 1 - y0 / tex_height,
0.5 * w, 0.5 * h)
return tex, uvmap
class Particle:
x = 0
y = 0
size = 1
def __init__(self, parent, i):
self.parent = parent
self.vsize = parent.vsize
self.base_i = 4 * i * self.vsize
self.reset(created=True)
def update(self):
for i in range(self.base_i,
self.base_i + 4 * self.vsize,
self.vsize):
self.parent.vertices[i:i + 3] = (
self.x, self.y, self.size)
def reset(self, created=False):
raise NotImplementedError()
def advance(self, nap):
raise NotImplementedError()
class GameScreen(Widget):
indices = []
vertices = []
particles = []
def __init__(self, **kwargs):
Widget.__init__(self, **kwargs)
self.canvas = RenderContext(use_parent_projection=True)
self.canvas.shader.source = "game.glsl"
self.vfmt = (
(b'vCenter', 2, 'float'),
(b'vScale', 1, 'float'),
(b'vPosition', 2, 'float'),
(b'vTexCoords0', 2, 'float'),
)
self.vsize = sum(attr[1] for attr in self.vfmt)
self.texture, self.uvmap = load_atlas()
def on_touch_down(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_down(touch)
def on_touch_move(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_move(touch)
def make_particles(self, Ap, num):
count = len(self.particles)
uv = self.uvmap[Ap.tex_name]
for i in range(count, count + num):
j = 4 * i
self.indices.extend((
j, j + 1, j + 2, j + 2, j + 3, j))
self.vertices.extend((
0, 0, 1, -uv.su, -uv.sv, uv.u0, uv.v1,
0, 0, 1, uv.su, -uv.sv, uv.u1, uv.v1,
0, 0, 1, uv.su, uv.sv, uv.u1, uv.v0,
0, 0, 1, -uv.su, uv.sv, uv.u0, uv.v0,
))
p = Ap(self, i)
self.particles.append(p)
def update_glsl(self, nap):
for p in self.particles:
p.advance(nap)
p.update()
self.canvas.clear()
with self.canvas:
Mesh(fmt=self.vfmt, mode='triangles',
indices=self.indices, vertices=self.vertices,
texture=self.texture)
class Ufo(Particle):
plane = 2.0
tex_name = 'Elien'
texture_size = 129
right = top = 129
def reset(self, created=False):
self.plane = random.uniform(2.0, 2.8)
self.x = random.randint(15, self.parent.right-15)
self.y = self.parent.top+random.randint(100, 2500)
self.size = random.uniform(0.5, 1.0) #every particle must have a random size
self.top = self.size * self.texture_size
self.right = self.size * self.texture_size
def collide_point(self, x, y):
'''Check if a point (x, y) is inside the Ufo's axis aligned bounding box.'''
with open('TouchFeedback.txt', 'wb') as c:
c.write(str(x)+', '+str(y))
return self.x <= x <= self.right and self.y <= y <= self.top
def advance(self, nap):
self.y -= 100 * self.plane * nap
if self.y < 0:
self.reset()
class Game(GameScreen):
def initialize(self):
self.make_particles(Ufo, 20)
def update_glsl(self, nap):
GameScreen.update_glsl(self, nap)
class GameApp(App):
def build(self):
EventLoop.ensure_window()
return Game()
def on_start(self):
self.root.initialize()
Clock.schedule_interval(self.root.update_glsl, 60 ** -1)
if __name__ == '__main__':
Window.clearcolor = get_color_from_hex('111110')
GameApp().run()
I think you are not calculating/updating your top and right properties of your Ufo objects. Also, since your GL code considers (x,y) to be the center of your circle, but Kivy considers (x,y) to be the lower left corner of an object, you need to keep that in mind. In order to correctly calculate collide_point(), I have added left and bottom properties to your Ufo, and use those properties to calculate collisions. Here is an updated version of your code with those changes:
from __future__ import division
import kivy
from kivy.config import Config
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
from kivy.lang import Builder
Config.set('modules', 'monitor', '')
from collections import namedtuple
import json
import math
import random
from kivy import platform
from kivy.app import App
from kivy.base import EventLoop
from kivy.clock import Clock
from kivy.core.image import Image
from kivy.core.window import Window
from kivy.event import EventDispatcher
from kivy.graphics import Mesh
from kivy.graphics.instructions import RenderContext
from kivy.properties import NumericProperty
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
import base64
UVMapping = namedtuple('UVMapping', 'u0 v0 u1 v1 su sv')
GLSL = """
---vertex
$HEADER$
attribute vec2 vCenter;
attribute float vScale;
void main(void)
{
tex_coord0 = vTexCoords0;
mat4 move_mat = mat4
(1.0, 0.0, 0.0, vCenter.x,
0.0, 1.0, 0.0, vCenter.y,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
vec4 pos = vec4(vPosition.xy * vScale, 0.0, 1.0)
* move_mat;
gl_Position = projection_mat * modelview_mat * pos;
}
---fragment
$HEADER$
void main(void)
{
gl_FragColor = texture2D(texture0, tex_coord0);
}
"""
with open("game.glsl", "wb") as glslc:
glslc.write(GLSL.encode())
def load_atlas():
atlas = json.loads('''{"game-0.png": {"Elien": [2, 26, 100, 100]}}''')
tex_name, mapping = atlas.popitem()
data = '''iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAACJklEQVR4nO3dy1ICQRAF0YLw/39ZVxMBGCjEMF23JvOsXPgounMaN61VQrtsH3x3TqFfLv9/ykdcq9z8RKv25Lro5yiUAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAfXUPsNM79ydWXbYZZVoAey7MPH6tQdScAI64KbV9T3QI6QGsuCKHDiH5l8DVd1aRd2QTT4DOjcCdBmknQMpTmDLH4ZICSFv0tHkOkRJA6mKnzvUxCQGkL3L6fLskBKBG3QFMebqmzPm2zgCmLeq0eV/SfQKoWVcAU5+mqXM/5QkA1xHA9Kdo+vx3PAHgDADOAOBWB3CW98+zvA5PADoDgDMAOAOAMwA4A4AzADgDgDMAuNUBnOXCxVlehycAnQHAGQBcRwDT3z+nz3/HEwCuK4CpT9HUuZ/yBIDrDGDa0zRt3pd0nwBTFnXKnG/rDkDNEgJIf7rS59slIYCq3EVOnetjUgKoylvstHkOkRRAVc6ip8xxuMS/E7gtfsflC8zGb9JOgFurNwO3+VWZJ8CtFacBcuM36QFsjggBvfGbKQFsHjfNfxix07QAHrmpOyX/EqgFDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwA7lrl7YpE7okkSZIkSZIkSZIkSZIkSZIkSZL+9AMvSSThyPfOhQAAAABJRU5ErkJggg=='''
with open(tex_name, "wb") as co:
co.write(base64.b64decode(data))
tex = Image(tex_name).texture
tex_width, tex_height = tex.size
uvmap = {}
for name, val in mapping.items():
x0, y0, w, h = val
x1, y1 = x0 + w, y0 + h
uvmap[name] = UVMapping(
x0 / tex_width, 1 - y1 / tex_height,
x1 / tex_width, 1 - y0 / tex_height,
0.5 * w, 0.5 * h)
return tex, uvmap
class Particle(EventDispatcher):
# x = 0
# y = 0
x = NumericProperty(0)
y = NumericProperty(0)
size = 1
def __init__(self, parent, i):
super(Particle, self).__init__()
self.parent = parent
self.vsize = parent.vsize
self.base_i = 4 * i * self.vsize
self.reset(created=True)
def update(self):
for i in range(self.base_i,
self.base_i + 4 * self.vsize,
self.vsize):
self.parent.vertices[i:i + 3] = (
self.x, self.y, self.size)
def reset(self, created=False):
raise NotImplementedError()
def advance(self, nap):
raise NotImplementedError()
class GameScreen(Widget):
indices = []
vertices = []
particles = []
def __init__(self, **kwargs):
Widget.__init__(self, **kwargs)
self.canvas = RenderContext(use_parent_projection=True)
self.canvas.shader.source = "game.glsl"
self.vfmt = (
(b'vCenter', 2, 'float'),
(b'vScale', 1, 'float'),
(b'vPosition', 2, 'float'),
(b'vTexCoords0', 2, 'float'),
)
self.vsize = sum(attr[1] for attr in self.vfmt)
self.texture, self.uvmap = load_atlas()
def on_touch_down(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_down(touch)
def on_touch_move(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_move(touch)
def make_particles(self, Ap, num):
count = len(self.particles)
uv = self.uvmap[Ap.tex_name]
for i in range(count, count + num):
j = 4 * i
self.indices.extend((
j, j + 1, j + 2, j + 2, j + 3, j))
self.vertices.extend((
0, 0, 1, -uv.su, -uv.sv, uv.u0, uv.v1,
0, 0, 1, uv.su, -uv.sv, uv.u1, uv.v1,
0, 0, 1, uv.su, uv.sv, uv.u1, uv.v0,
0, 0, 1, -uv.su, uv.sv, uv.u0, uv.v0,
))
p = Ap(self, i)
self.particles.append(p)
def update_glsl(self, nap):
for p in self.particles:
p.advance(nap)
p.update()
self.canvas.clear()
self.canvas.before.clear() # temporary
with self.canvas.before: # temporary code block
for p in self.particles:
Rectangle(pos=(p.left, p.bottom), size=(p.size*p.texture_size, p.size*p.texture_size))
with self.canvas:
Mesh(fmt=self.vfmt, mode='triangles',
indices=self.indices, vertices=self.vertices,
texture=self.texture)
class Ufo(Particle):
plane = 2.0
tex_name = 'Elien'
texture_size = 129
right = NumericProperty(129)
top = NumericProperty(129)
left = NumericProperty(0)
bottom = NumericProperty(0)
def reset(self, created=False):
self.plane = random.uniform(2.0, 2.8)
self.size = random.uniform(0.5, 1.0) #every particle must have a random size
self.x = random.randint(15, self.parent.right-15)
self.y = self.parent.top+random.randint(100, 2500)
def collide_point(self, x, y):
'''Check if a point (x, y) is inside the Ufo's axis aligned bounding box.'''
return self.left <= x <= self.right and self.bottom <= y <= self.top
def advance(self, nap):
self.y -= 100 * self.plane * nap
if self.y < 0:
self.reset()
def on_x(self, instance, new_x):
self.right = new_x + self.size * self.texture_size / 2.0
self.left = new_x - self.size * self.texture_size / 2.0
def on_y(self, instance, new_y):
self.top = new_y + self.size * self.texture_size / 2.0
self.bottom = new_y - self.size * self.texture_size / 2.0
class Game(GameScreen):
def initialize(self):
self.make_particles(Ufo, 20)
def update_glsl(self, nap):
GameScreen.update_glsl(self, nap)
class GameApp(App):
def build(self):
EventLoop.ensure_window()
return Game()
def on_start(self):
self.root.initialize()
Clock.schedule_interval(self.root.update_glsl, 60 ** -1)
if __name__ == '__main__':
Window.clearcolor = get_color_from_hex('111110')
GameApp().run()
I have also adding drawing the Ufo bounding box using canvas.before. This is just to visualize the clickable area for each Ufo, and can be easily removed.
Try using collide_widget()
Snippets
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
for w in self.particles:
if self.collide_widget(w):
w.reset()
return True
return super(GameScreen, self).on_touch_down(touch)
Widget class ยป collide_widget
collide_widget(wid)
Check if another widget collides with this widget. This function performs an axis-aligned bounding box intersection test by default.
Parameters:
wid: Widget class
Widget to test collision with.
Returns:
bool. True if the other widget collides with this widget, False otherwise.

How to implement camera pan like in 3dsMax?

What are the necessary maths to achieve the camera panning effect that's used in 3ds max?
In 3ds max the distance between the cursor and the mesh will always remain the same throughout the entire movement (mouse_down+mouse_motion+mouse_up).
My naive and failed attempt has been trying to move the camera on the plane XY by using dt (frame time) multiplied by some hardcoded constant and the result is really ugly and uintuitive.
The code I've got so far is:
def glut_mouse(self, button, state, x, y):
self.last_mouse_pos = vec2(x, y)
self.mouse_down_pos = vec2(x, y)
def glut_motion(self, x, y):
pos = vec2(x, y)
move = self.last_mouse_pos - pos
self.last_mouse_pos = pos
self.pan(move)
def pan(self, delta):
forward = vec3.normalize(self.target - self.eye)
right = vec3.normalize(vec3.cross(forward, self.up))
up = vec3.normalize(vec3.cross(forward, right))
if delta.x:
right = right*delta.x
if delta.y:
up = up*delta.y
self.eye+=(right+up)
self.target+=(right+up)
Could you explain how the maths of camera panning in 3dsmax work?
EDIT:
My question has already been answered initially by #Rabbid76 but there's still one case where his algorithm won't work properly. It doesn't handle properly the case where you panning is started from empty space (said otherwise, when depth buffer value takes the far value=1.0). In 3dsmax camera panning is handled correctly in all situations, no matter which value of the depth buffer.
Your solution would work at orthographic projection, but it fails at perspective projection. Note, at Perspective Projection the projection matrix describes the mapping from 3D points in the world as they are seen from of a pinhole camera, to 2D points of the viewport.
The amount of displacement for the eye and target position depends on the depth of the object which is dragged on the viewport.
If the object is close to the eye position, then a translation on the viewport leads to a small displacement of the eye and target positions:
If the distance from the object to the eye is far, then a translation on the viewport leads to a large displacement of the eye and target positions:
To do what you want you have to know the size of the viewport, the view matrix and the projection matrix:
self.width # width of the viewport
self.height # height of the viewport
self.view # view matrix
self.proj # prjection matrix
Change the pane method, so that it receives the new and old mouse position. Note y axis has to be flipped (self.height-y). Get the depth of the hit point (object) by glReadPixels using the format type GL_DEPTH_COMPONENT:
def glut_mouse(self, button, state, x, y):
self.drag = state == GLUT_DOWN
self.last_mouse_pos = glm.vec2(x, self.height-y)
self.mouse_down_pos = glm.vec2(x, self.height-y)
if self.drag:
depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
self.last_depth = depth_buffer[0][0]
print(self.last_depth)
def glut_motion(self, x, y):
if not self.drag:
return
old_pos = self.last_mouse_pos
new_pos = glm.vec2(x, self.__vp_size[1]-y)
self.last_mouse_pos = new_pos
self.pan(self.last_depth, old_pos, new_pos)
def pan(self, depth, old_pos, new_pos):
# .....
The mouse position gives a position in window space, where the z coordinate is the depth of the hit point respectively object:
wnd_from = glm.vec3(old_pos[0], old_pos[1], float(depth))
wnd_to = glm.vec3(new_pos[0], new_pos[1], float(depth))
This positions can be transformed to world space by glm.unProject:
vp_rect = glm.vec4(0, 0, self.width, self.height)
world_from = glm.unProject(wnd_from, self.view, self.proj, vp_rect)
world_to = glm.unProject(wnd_to, self.view, self.proj, vp_rect)
The world space displacement of the eye and target position is the distance from the old to the new world position:
world_vec = world_to - world_from
Finally calculate the new eye and target position and update the view matrix:
self.eye = self.eye - world_vec
self.target = self.target - world_vec
self.view = glm.lookAt(self.eye, self.target, self.up)
See also Python OpenGL 4.6, GLM navigation
I tested the code with the following example:
Preview:
Full python code:
import os
import math
import numpy as np
import glm
from OpenGL.GLUT import *
from OpenGL.GL import *
from OpenGL.GL.shaders import *
from OpenGL.arrays import *
from ctypes import c_void_p
class MyWindow:
__caption = 'OpenGL Window'
__vp_size = [800, 600]
__vp_valid = False
__glut_wnd = None
__glsl_vert = """
#version 450 core
layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec3 a_nv;
layout (location = 2) in vec4 a_col;
out vec3 v_pos;
out vec3 v_nv;
out vec4 v_color;
uniform mat4 u_proj;
uniform mat4 u_view;
uniform mat4 u_model;
void main()
{
mat4 model_view = u_view * u_model;
mat3 normal = transpose(inverse(mat3(model_view)));
vec4 view_pos = model_view * vec4(a_pos.xyz, 1.0);
v_pos = view_pos.xyz;
v_nv = normal * a_nv;
v_color = a_col;
gl_Position = u_proj * view_pos;
}
"""
__glsl_frag = """
#version 450 core
out vec4 frag_color;
in vec3 v_pos;
in vec3 v_nv;
in vec4 v_color;
void main()
{
vec3 N = normalize(v_nv);
vec3 V = -normalize(v_pos);
float ka = 0.1;
float kd = max(0.0, dot(N, V)) * 0.9;
frag_color = vec4(v_color.rgb * (ka + kd), v_color.a);
}
"""
__program = None
__vao = None
__vbo = None
__no_vert = 0
def __init__(self, w, h):
self.__vp_size = [w, h]
glutInit()
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
glutInitWindowSize(self.__vp_size[0], self.__vp_size[1])
__glut_wnd = glutCreateWindow(self.__caption)
self.__program = compileProgram(
compileShader( self.__glsl_vert, GL_VERTEX_SHADER ),
compileShader( self.__glsl_frag, GL_FRAGMENT_SHADER ),
)
self.___attrib = { a : glGetAttribLocation (self.__program, a) for a in ['a_pos', 'a_nv', 'a_col'] }
print(self.___attrib)
self.___uniform = { u : glGetUniformLocation (self.__program, u) for u in ['u_model', 'u_view', 'u_proj'] }
print(self.___uniform)
v = [ -1,-1,1, 1,-1,1, 1,1,1, -1,1,1, -1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1 ]
c = [ 1.0, 0.0, 0.0, 1.0, 0.5, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ]
n = [ 0,0,1, 1,0,0, 0,0,-1, -1,0,0, 0,1,0, 0,-1,0 ]
e = [ 0,1,2,3, 1,5,6,2, 5,4,7,6, 4,0,3,7, 3,2,6,7, 1,0,4,5 ]
attr_array = []
for si in range(6):
for vi in range(6):
ci = [0, 1, 2, 0, 2, 3][vi]
i = si*4+ci
attr_array.extend( [ v[e[i]*3], v[e[i]*3+1], v[e[i]*3+2] ] )
attr_array.extend( [ n[si*3], n[si*3+1], n[si*3+2] ] )
attr_array.extend( [ c[si*3], c[si*3+1], c[si*3+2], 1 ] );
self.__no_vert = len(attr_array) // 10
vertex_attributes = np.array(attr_array, dtype=np.float32)
self.__vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.__vbo)
glBufferData(GL_ARRAY_BUFFER, vertex_attributes, GL_STATIC_DRAW)
self.__vao = glGenVertexArrays(1)
glBindVertexArray(self.__vao)
glVertexAttribPointer(0, 3, GL_FLOAT, False, 10*vertex_attributes.itemsize, None)
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, False, 10*vertex_attributes.itemsize, c_void_p(3*vertex_attributes.itemsize))
glEnableVertexAttribArray(1)
glVertexAttribPointer(2, 4, GL_FLOAT, False, 10*vertex_attributes.itemsize, c_void_p(6*vertex_attributes.itemsize))
glEnableVertexAttribArray(2)
glEnable(GL_DEPTH_TEST)
glUseProgram(self.__program)
glutReshapeFunc(self.__reshape)
glutDisplayFunc(self.__mainloop)
glutMouseFunc(self.glut_mouse)
glutMotionFunc(self.glut_motion)
self.drag = False
self.eye = glm.vec3(-3, -7, 6)
self.target = glm.vec3(0, 0, 0)
self.up = glm.vec3(0, 0, 1)
self.near = 0.1
self.far = 100.0
aspect = self.__vp_size[0]/self.__vp_size[1]
self.proj = glm.perspective(glm.radians(90.0), aspect, self.near, self.far)
self.view = glm.lookAt(self.eye, self.target, self.up)
self.model = glm.mat4(1)
def run(self):
self.__starttime = 0
self.__starttime = self.elapsed_ms()
glutMainLoop()
def elapsed_ms(self):
return glutGet(GLUT_ELAPSED_TIME) - self.__starttime
def __reshape(self, w, h):
self.__vp_valid = False
def __mainloop(self):
if not self.__vp_valid:
self.width = glutGet(GLUT_WINDOW_WIDTH)
self.height = glutGet(GLUT_WINDOW_HEIGHT)
self.__vp_size = [self.width, self.height]
self.__vp_valid = True
aspect = self.width / self.height
self.proj = glm.perspective(glm.radians(90.0), aspect, self.near, self.far)
glUniformMatrix4fv(self.___uniform['u_proj'], 1, GL_FALSE, glm.value_ptr(self.proj) )
glUniformMatrix4fv(self.___uniform['u_view'], 1, GL_FALSE, glm.value_ptr(self.view) )
glUniformMatrix4fv(self.___uniform['u_model'], 1, GL_FALSE, glm.value_ptr(self.model) )
glClearColor(0.2, 0.3, 0.3, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glDrawArrays(GL_TRIANGLES, 0, self.__no_vert)
glutSwapBuffers()
glutPostRedisplay()
def glut_mouse(self, button, state, x, y):
self.drag = state == GLUT_DOWN
self.last_mouse_pos = glm.vec2(x, self.height-y)
self.mouse_down_pos = glm.vec2(x, self.height-y)
if self.drag:
depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
self.last_depth = depth_buffer[0][0]
print(self.last_depth)
def glut_motion(self, x, y):
if not self.drag:
return
old_pos = self.last_mouse_pos
new_pos = glm.vec2(x, self.__vp_size[1]-y)
self.last_mouse_pos = new_pos
self.pan(self.last_depth, old_pos, new_pos)
def pan(self, depth, old_pos, new_pos):
wnd_from = glm.vec3(old_pos[0], old_pos[1], float(depth))
wnd_to = glm.vec3(new_pos[0], new_pos[1], float(depth))
vp_rect = glm.vec4(0, 0, self.width, self.height)
world_from = glm.unProject(wnd_from, self.view, self.proj, vp_rect)
world_to = glm.unProject(wnd_to, self.view, self.proj, vp_rect)
world_vec = world_to - world_from
self.eye = self.eye - world_vec
self.target = self.target - world_vec
self.view = glm.lookAt(self.eye, self.target, self.up)
window = MyWindow(800, 600)
window.run()
[...] but there's still one case where his algorithm won't work properly. It doesn't handle properly the case where you panning is started from empty space [...]
In the solution the depth of the object is taken from the depth buffer, at that position, where the mouse click occurs. If this is the "empty space", a position where no object was drawn, the depth is the maximum of the depth range (in common 1). This leads to a rapid paining.
A solution or workaround would be use the depth of an representative position of the scene. e.g. the origin of the world:
pt_drag = glm.vec3(0, 0, 0)
Of course this may not lead to a proper result in each case. If the objects of the scene are not around the origin of the world, this approach will fail. I recommend to calculate the center of the axis aligned bounding box of the scene. Use this point for the representative "depth":
box_min = ... # glm.vec3
box_max = ... # glm.vec3
pt_drag = (box_min + box_max) / 2
The depth of a point can computed by the transformation with the view and projection matrix and a final perspective divide:
o_clip = self.proj * self.view * glm.vec4(pt_drag, 1)
o_ndc = glm.vec3(o_clip) / o_clip.w
This can be applied to the function glut_mouse:
def glut_mouse(self, button, state, x, y):
self.drag = state == GLUT_DOWN
self.last_mouse_pos = glm.vec2(x, self.height-y)
self.mouse_down_pos = glm.vec2(x, self.height-y)
if self.drag:
depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
self.last_depth = depth_buffer[0][0]
if self.last_depth == 1:
pt_drag = glm.vec3(0, 0, 0)
o_clip = self.proj * self.view * glm.vec4(pt_drag, 1)
o_ndc = glm.vec3(o_clip) / o_clip.w
if o_ndc.z > -1 and o_ndc.z < 1:
self.last_depth = o_ndc.z * 0.5 + 0.5
Preview:
The key to a well feeling solution is to find the "correct" depth. At perspective projection the dragging, where the mouse movement effects the object in a 1:1 motion, projected on the viewport, only works correctly for a well defined depth. Objects with different depths are displaced by a different scale when they projected on the viewport, that's the "nature" of perspective.
To find the "correct" depth, there are different possibilities, which depend on your needs:
Reading the depth from the depth buffer at the current mouse position:
depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
self.last_depth = depth_buffer[0][0]
Get the minimum and maximum depth of the depth buffer (except the value for the far plane, 1.0) and calculate the mean depth. Of course the entire depth buffer has to be investigated in this case:
d_buf = glReadPixels(0, 0, self.width, self.height, GL_DEPTH_COMPONENT, GL_FLOAT)
d_vals = [float(d_buf[i][j]) for i in range(self.width) for j in range(self.height) if d_buf[i][j] != 1]
if len(d_vals) > 0:
self.last_depth = (min(d_vals) + max(d_vals)) / 2
Use the origin of the world:
pt_drag = glm.vec3(0, 0, 0)
o_clip = self.proj * self.view * glm.vec4(pt_drag, 1)
o_ndc = glm.vec3(o_clip) / o_clip.w
if o_ndc.z > -1 and o_ndc.z < 1:
self.last_depth = o_ndc.z * 0.5 + 0.5
Calculating the center of the bounding box of the scene.
Implement a raycasting, which identifies an object by a ray, which starts at the point of view a runs trough the cursor (mouse) position. This algorithm can be advanced by identifying the object which is "closest" to the ray, when no object is hit.
See also Python OpenGL 4.6, GLM navigation

Categories

Resources