简体   繁体   中英

How to generate a torus knot?

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM