4

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:

showcase

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 :)

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
BPL
  • 9,632
  • 9
  • 59
  • 117
  • `T = (nextpoint - centerpoint), N = (nextpoint + centerpoint)` – odd way to generate local basis vectors; do you have a source for this particular method? – meowgoesthedog Jan 14 '19 at 11:43
  • @meowgoesthedog Yeah, the main sources for that method are coming from [https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas](https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas). Also, worth noting there is this article [http://blackpawn.com/texts/pqtorus/default.html](http://blackpawn.com/texts/pqtorus/default.html). Although as you can see in this last link the torus-knot's formulas aren't like the original one from the wiki though, which are the ones I'd like to use here. – BPL Jan 14 '19 at 11:49

1 Answers1

5

... 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

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • 2
    Yeah, meowgoesthedog is correct.... BUT, Rabbid76 has already solved one of the main and more important issues here, which is the fact I was misinterpreting the ferret-serret formula in my code, so just for that I'll give him +1. Remaining thing for this answer to be complete is using the original torus-knot sampling formula from the wiki and showing how to create the mesh connectivity... which is basically the other part I'm struggling with ;) – BPL Jan 14 '19 at 12:00
  • 1
    My bad for not reading the full answer; this seems to have solved OP's only problem, and all that remains is to replace `sample` with OP's function for knots. +1 from me too. – meowgoesthedog Jan 14 '19 at 12:09
  • Interesting [article](https://janakiev.com/blog/framing-parametric-curves/) that also talks about some possible issues we'll find when using Frenet frames. That said, I think the trickiest math part here is how to compute properly the TBN frame, ie: `T=x'(t)/|x'(t)|, B=cross(x'(t),x''(t))/|cross(x'(t),x''(t))|, N=cross(B,T)`. The 1st and 2nd derivatives (`x'(t) and x''(t)`)... in that article they use `numpy.gradient`... i'll think how to approximate these derivatives without any external package like numpy though. – BPL Jan 14 '19 at 13:11
  • Guess using 4 points for sampling the 2nd derivative may be good enough [https://math.stackexchange.com/a/654574/490376](https://math.stackexchange.com/a/654574/490376) – BPL Jan 14 '19 at 13:27
  • @Rabbid76 Thanks! I'll take a look, in the meantime I've also found these articles as well that may be relevants, [article1](https://en.wikipedia.org/wiki/Finite_difference), [article2](https://scicomp.stackexchange.com/questions/480/how-can-i-numerically-differentiate-an-unevenly-sampled-function) and [article3](https://mathformeremortals.wordpress.com/2013/01/12/a-numerical-second-derivative-from-three-points/) :) – BPL Jan 14 '19 at 14:22
  • @Rabbid76 It's the first time I've heard about the Trefoil Knot, really interesting stuff :) . So... if I've understood correctly, are both Torus and TrefoilKnot particular cases of a TorusKnot? (ie: `torus=(1,0)-torus knot` and `trefoil knot=(2,3)-torus knot`) . Or that's not totally correct? Asking it cos that way we could use a well defined torus-knot to represent all these interesting type of 3d meshes. In fact, it looks to me like a torus knot is a simple particular case of using frenet-frames for a particular parametric curve using a particular shape (circle in this case) – BPL Jan 14 '19 at 14:39
  • @Rabbid76 Before validating your already more than good enough answer... let me ask you, would it be too difficult to add the bits that generate the mesh connectivity? Btw... those colors gradients are cool, which function did you use? :) – BPL Jan 14 '19 at 20:31
  • @BPL I extended the answer. By the way try `p, q = 2, 5`. – Rabbid76 Jan 14 '19 at 21:55