Let me start the question by providing some boilerplate code we'll use to play around:
mcve_framework.py
import time
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import glm
from glm import unProject
from glm import vec2
from glm import vec3
from glm import vec4
# -------- Camera --------
class BaseCamera():
def __init__(
self,
eye=None, target=None, up=None,
fov=None, near=0.1, far=100000
):
self.eye = eye or glm.vec3(0, 0, 1)
self.target = target or glm.vec3(0, 0, 0)
self.up = up or glm.vec3(0, 1, 0)
self.original_up = glm.vec3(self.up)
self.fov = fov or glm.radians(45)
self.near = near
self.far = far
def update(self, aspect):
self.view = glm.lookAt(
self.eye, self.target, self.up
)
self.projection = glm.perspective(
self.fov, aspect, self.near, self.far
)
# def zoom(self, *args):
# delta = -args[1] * 0.1
# self.eye = self.target + (self.eye - self.target) * (delta + 1)
def zoom(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.w
pt_wnd = vec3(x, height - y, 1.0)
pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
ray_cursor = glm.normalize(pt_world - self.eye)
delta = args[1] * 10
self.eye = self.eye + ray_cursor * delta
self.target = self.target + ray_cursor * delta
def load_projection(self):
width = glutGet(GLUT_WINDOW_WIDTH)
height = glutGet(GLUT_WINDOW_HEIGHT)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(glm.degrees(self.fov), width / height, self.near, self.far)
def load_modelview(self):
e = self.eye
t = self.target
u = self.up
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)
class Camera(BaseCamera):
def rotate_target(self, delta):
right = glm.normalize(glm.cross(self.target - self.eye, self.up))
M = glm.mat4(1)
M = glm.translate(M, self.eye)
M = glm.rotate(M, delta.y, right)
M = glm.rotate(M, delta.x, self.up)
M = glm.translate(M, -self.eye)
self.target = glm.vec3(M * glm.vec4(self.target, 1.0))
def rotate_around_target(self, target, delta):
right = glm.normalize(glm.cross(self.target - self.eye, self.up))
amount = (right * delta.y + self.up * delta.x)
M = glm.mat4(1)
M = glm.rotate(M, amount.z, glm.vec3(0, 0, 1))
M = glm.rotate(M, amount.y, glm.vec3(0, 1, 0))
M = glm.rotate(M, amount.x, glm.vec3(1, 0, 0))
self.eye = glm.vec3(M * glm.vec4(self.eye, 1.0))
self.target = target
self.up = self.original_up
def rotate_around_origin(self, delta):
return self.rotate_around_target(glm.vec3(0), delta)
class GlutController():
FPS = 0
ORBIT = 1
def __init__(self, camera, velocity=100, velocity_wheel=100):
self.velocity = velocity
self.velocity_wheel = velocity_wheel
self.camera = camera
def glut_mouse(self, button, state, x, y):
self.mouse_last_pos = vec2(x, y)
self.mouse_down_pos = vec2(x, y)
if button == GLUT_LEFT_BUTTON:
self.mode = self.FPS
elif button == GLUT_RIGHT_BUTTON:
self.mode = self.ORBIT
def glut_motion(self, x, y):
pos = vec2(x, y)
move = self.mouse_last_pos - pos
self.mouse_last_pos = pos
if self.mode == self.FPS:
self.camera.rotate_target(move * 0.005)
elif self.mode == self.ORBIT:
self.camera.rotate_around_origin(move * 0.005)
def glut_mouse_wheel(self, *args):
self.camera.zoom(*args)
# -------- Miscelanea --------
def render_text(x, y, text):
glColor3f(1, 1, 1)
glRasterPos2f(x, y)
glutBitmapString(GLUT_BITMAP_TIMES_ROMAN_24, text.encode("utf-8"))
def line(p0, p1, color=None):
c = color or glm.vec3(1, 1, 1)
glColor3f(c.x, c.y, c.z)
glVertex3f(p0.x, p0.y, p0.z)
glVertex3f(p1.x, p1.y, p1.z)
def grid(segment_count=10, spacing=1, yup=True):
size = segment_count * spacing
right = glm.vec3(1, 0, 0)
forward = glm.vec3(0, 0, 1) if yup else glm.vec3(0, 1, 0)
x_axis = right * size
z_axis = forward * size
i = -segment_count
glBegin(GL_LINES)
while i <= segment_count:
p0 = -x_axis + forward * i * spacing
p1 = x_axis + forward * i * spacing
line(p0, p1)
p0 = -z_axis + right * i * spacing
p1 = z_axis + right * i * spacing
line(p0, p1)
i += 1
glEnd()
def axis(size=1.0, yup=True):
right = glm.vec3(1, 0, 0)
forward = glm.vec3(0, 0, 1) if yup else glm.vec3(0, 1, 0)
x_axis = right * size
z_axis = forward * size
y_axis = glm.cross(forward, right) * size
glBegin(GL_LINES)
line(x_axis, glm.vec3(0, 0, 0), glm.vec3(1, 0, 0))
line(y_axis, glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
line(z_axis, glm.vec3(0, 0, 0), glm.vec3(0, 0, 1))
glEnd()
# -------- Mcve --------
class BaseWindow:
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)
glutMouseFunc(self.controller.glut_mouse)
glutMotionFunc(self.controller.glut_motion)
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']:
if key == '1':
self.index_camera = "BPL"
self.camera = self.cameras[self.index_camera]
self.controller.camera = self.camera
except Exception as e:
import traceback
traceback.print_exc()
def display(self):
pass
def startup(self):
pass
def _startup(self):
glEnable(GL_DEPTH_TEST)
params = {
"eye": glm.vec3(0, 150, 150),
"target": glm.vec3(0, 0, 0),
"up": glm.vec3(0, 1, 0)
}
self.start_time = time.time()
self.cameras = {
"BPL": Camera(**params)
}
self.index_camera = "BPL"
self.yup = True
self.camera = self.cameras[self.index_camera]
self.model = glm.mat4(1)
self.controller = GlutController(self.camera)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
self.startup()
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 render_points(self, vertices):
glColor3f(0.0, 0.0, 0.0)
glBegin(GL_POINTS)
for v in vertices:
glVertex3f(v.x, v.y, v.z)
glEnd()
def render_triangles(self, vertices):
glBegin(GL_TRIANGLES)
for i in range(0, len(vertices), 3):
v0 = vertices[i]
v1 = vertices[i + 1]
v2 = vertices[i + 2]
glVertex3f(v0.x, v0.y, v0.z)
glVertex3f(v1.x, v1.y, v1.z)
glVertex3f(v2.x, v2.y, v2.z)
glEnd()
def render_quads(self, vertices):
glBegin(GL_QUADS)
for i in range(0, len(vertices), 4):
v0 = vertices[i]
v1 = vertices[i + 1]
v2 = vertices[i + 2]
v3 = vertices[i + 3]
glVertex3f(v0.x, v0.y, v0.z)
glVertex3f(v1.x, v1.y, v1.z)
glVertex3f(v2.x, v2.y, v2.z)
glVertex3f(v3.x, v3.y, v3.z)
glEnd()
def render_indexed_triangles(self, indices, vertices):
glBegin(GL_TRIANGLES)
for i in indices:
for j in range(3):
v = vertices[i[j]]
glVertex3f(v.x, v.y, v.z)
glEnd()
def render_indexed_quads(self, indices, vertices):
glBegin(GL_QUADS)
for f1, f2 in zip(indices[::2], indices[1::2]):
i = [f1[0], f1[1], f1[2], f2[2]]
for j in range(4):
v = vertices[i[j]]
glVertex3f(v.x, v.y, v.z)
glEnd()
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)
self.camera.load_projection()
self.camera.load_modelview()
self.display()
glLineWidth(5)
axis(size=70, yup=True)
glLineWidth(1)
grid(segment_count=7, spacing=10, yup=True)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-1, 1, -1, 1, -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
info = "\n".join([
"{}: Camera - {}".format(i, k) for i, k in enumerate(self.cameras.keys())
])
render_text(-1.0, 1.0 - 0.1, info)
render_text(-1.0, -1.0, "{} camera is active".format(self.index_camera))
glutSwapBuffers()
In case you want to use the above code you'll just need to install pyopengl and pygml. After that, you can just create your own BaseWindow
subclass, override startup
and render
and you should have a very basic glut window with simple functionality such as camera rotation/zooming as well as some methods to render points/triangles/quads and indexed_triangles/indexed_quads.
QUESTION
And now the real question, consider this little snippet:
mcve_torusknot.py
from math import cos
from math import pi
from math import sin
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from glm import cross
from glm import normalize
from glm import vec3
from mcve_framework import BaseWindow
def sample(theta, p, q, out):
r = cos(q * theta) + 2.0
out.x = r * cos(p * theta)
out.y = r * sin(q * theta)
out.z = -sin(q * theta)
def gen_torusknot(tess_u, tess_v, p, q):
vertices = []
pp = vec3()
centerpoint = vec3()
nextpoint = vec3()
T = vec3()
B = vec3()
N = vec3()
r2 = 5.0
for u in range(tess_u):
theta = (u / tess_u) * 2 * pi
sample(theta, p, q, centerpoint)
theta = (u + 1) * 2 * pi / tess_u
sample(theta, p, q, nextpoint)
T = (nextpoint - centerpoint)
N = (nextpoint + centerpoint)
B = normalize(cross(T, N))
N = normalize(cross(B, T))
for v in range(tess_v):
theta = (v / tess_v) * 2 * pi
pointx = sin(theta) * r2
pointy = cos(theta) * r2
pp = N * pointx + B * pointy + centerpoint
vertices.append(pp * 10)
return vertices
class McveTorusKnot(BaseWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def startup(self):
self.torusknot = gen_torusknot(60, 60, 1.000000001, 0.000000001)
def display(self):
glPointSize(3)
glPushMatrix()
self.render_points(self.torusknot)
glPopMatrix()
if __name__ == '__main__':
window = McveTorusKnot(800, 600)
window.run()
The end goal here is to figure out how to generate and render a torus knots. But before such an ambitious goal I'd like to figure out why when using p=1
and q=0
parameters I'm not getting a simple torus like the one shown here https://www.geeks3d.com/20140516/pq-torus-knot/, instead I'm get something like this:
So yeah, that's basically my question, first of all, I'd like to know what's wrong in my above code so I'm not getting a simple torus from the general formula and after that... I'd like to know what's the way to create the mesh connectivity (aka indices, no matter triangles/quads/triangle strips)?
Note: For the sake of simplicity at this point normals or texture coordinates are irrelevant, just to know how to generate the position vertices/indices of the mesh properly will be more than good enough :)
... first of all, I'd like to know what's wrong in my above code so I'm not getting a simple torus ...
The function sample just has to generate a point on a circle with radius r
:
def sample(theta, p, q, out):
r = 5.0
out.x = r * cos(theta)
out.y = r * sin(theta)
out.z = 0
The tangent (T
) is the vector from the current point on the circle to the next point (approximated), but the bitangent (B
) is the vector from the center of the torus to the current point on the circle:
def gen_torusknot(tess_u, tess_v, p, q):
vertices = []
pp = vec3()
centerpoint = vec3()
nextpoint = vec3()
T = vec3()
B = vec3()
N = vec3()
r2 = 2.0
for u in range(tess_u):
theta = (u / tess_u) * 2 * pi
sample(theta, p, q, centerpoint)
theta = (u + 1) * 2 * pi / tess_u
sample(theta, p, q, nextpoint)
T = (nextpoint - centerpoint)
B = normalize(centerpoint)
N = normalize(cross(T, B))
T = normalize(cross(B, N))
for v in range(tess_v):
theta = (v / tess_v) * 2 * pi
pointx = sin(theta) * r2
pointy = cos(theta) * r2
pp = N * pointx + B * pointy + centerpoint
vertices.append(pp * 10)
return vertices
The first step ahead to the Torus knot is to sample points on the geometrical representation and sample points on a circle in the center of the torus.
The points on the circle can be calculated in the same way as the points on the tours, except that the radius of the tube (r2
) is 0.
def sample(phi, p, q, r1, r2, out):
out.x = (r1 + r2 * cos(p * phi)) * cos(q * phi)
out.y = (r1 + r2 * cos(p * phi)) * sin(q * phi)
out.z = r2 * -sin(p * phi)
def gen_torusknot(tess_u, tess_v, p, q):
vertices = []
pt_tk = vec3()
pt_c = vec3()
r1, r2 = 5, 2
p, q = 7, 3
for u in range(tess_u):
phi = (u / tess_u) * 2 * pi
sample(phi, p, q, r1, r2, pt_tk)
sample(phi, p, q, r1, 0, pt_c)
vertices.append(pt_tk * 10)
vertices.append(pt_c * 10)
return vertices
Finally the tangent (T
) and bitangent (B
) can be calculated similar to that ones of the the torus.
The tangent (T) is the vector from the current point on the torus-knot to the next point (approximated) on the torus-knot. The bitangent (B) is the vector from the sample point on the circle to the current sample point on the torus-knot:
def sample(phi, p, q, r1, r2, out):
out.x = (r1 + r2 * cos(p * phi)) * cos(q * phi)
out.y = (r1 + r2 * cos(p * phi)) * sin(q * phi)
out.z = r2 * -sin(p * phi)
def gen_torusknot(tess_u, tess_v, p, q):
vertices = []
pt_tk = vec3()
pt_tk_next = vec3()
pt_c = vec3()
r1, r2, r3 = 5, 2, 0.5
p, q = 7, 3
for u in range(tess_u):
phi = (u / tess_u) * 2 * pi
sample(phi, p, q, r1, r2, pt_tk)
phi = (u + 1) * 2 * pi / tess_u
sample(phi, p, q, r1, r2, pt_tk_next)
sample(phi, p, q, r1, 0, pt_c)
T = (pt_tk_next - pt_tk)
B = normalize(pt_tk - pt_c)
N = normalize(cross(T, B))
T = normalize(cross(B, N))
for v in range(tess_v):
theta = (v / tess_v) * 2 * pi
px = sin(theta) * r3
py = cos(theta) * r3
pp = N * px + B * py + pt_tk
vertices.append(pp * 10)
return vertices
Setting p = 3
and q = 2
will generate a trefoil knot and p = 0
and q = 1
will generate a torus.
I activated multisampling, to generate the above images.
glutInit()
glutSetOption( GLUT_MULTISAMPLE, 8 )
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_MULTISAMPLE)
Further, I used a function which converts a Hue value in range [0, 1] to a RGB color
def HUEtoRGB(self, H):
R = abs(H * 6.0 - 3.0) - 1.0
G = 2.0 - abs(H * 6.0 - 2.0)
B = 2.0 - abs(H * 6.0 - 4.0)
return (R, G, B)
and the following function to draw the points:
def render_points(self, vertices):
glPointSize(5)
glBegin(GL_POINTS)
for i in range(len(vertices)):
v = vertices[i]
H = i / len(vertices)
glColor4f(*self.HUEtoRGB(H), 0.0)
glVertex3f(v.x, v.y, v.z)
glEnd()
A mesh instead of points can be generated by the primitive GL_TRIANLGL_STRIP
. tess_u
* strips are rendered along the knot curve, each strip consists of tess_v
sections (tess_v
* 2 triangles):
def render_strips(self, vertices, tess_v):
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
no_strips = len(vertices) // tess_v
for i_strip in range(no_strips):
glBegin(GL_TRIANGLE_STRIP)
for i_v in range(tess_v+1):
for i in [i_strip, (i_strip+1) % no_strips]:
v, H = vertices[i*tess_v + i_v % tess_v], i / no_strips
glColor4f(*self.HUEtoRGB(H), 0.0)
glVertex3f(v.x, v.y, v.z)
glEnd()
class McveTorusKnot(BaseWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def startup(self):
self.tess_v = 60
self.torusknot = gen_torusknot(300, self.tess_v, 1.000000001, 0.000000001)
def display(self):
glPushMatrix()
#self.render_points(self.torusknot)
self.render_strips(self.torusknot, self.tess_v)
glPopMatrix()
The following trefoil knot was rendered by the use of the following parameters:
r1, r2, r3 = 5, 2, 1
p, q = 3, 2