pythonpygamecubic-bezier

How to determine the ideal increment size of t value for a Bezier curve


I was playing around with Bezier curves trying to understand it.

Here is the code.

import pygame
import sys

pygame.init()

textSize = 25
font = pygame.font.Font(pygame.font.get_default_font(), textSize)
_t = 0

red = (255, 0, 0)
green = (0, 255 , 0)
blue = (0, 0 , 255)
black = (0, 0 , 0)

def drawPoint(window, p, color):
    pygame.draw.circle(window, color, (p.x, p.y), p.radius)

def getPs(t):
    tt = t * t
    ttt = t * t * t
    return(
        -ttt + 3 * tt -3 * t + 1,
        3 * ttt - 6 * tt + 3 * t,
        -3 * ttt + 3 * tt,
        ttt
    ) 

def drawPointOnCurve(window, p, t, color, scale):
    pygame.draw.circle(window, color, (5 + (t * scale), 50 + (p * scale)), 5)

def drawCurvePixel(window, p, t, color, scale):
    window.set_at((5 + int(t * scale), 50 + int(p * scale)), color)

def drawVectorsAdded(window, origin, vecs):
    p0End = (vecs[0][0] + origin[0], vecs[0][1] + origin[1])
    p1End = (vecs[1][0] + p0End[0], vecs[1][1] + p0End[1])
    p2End = (vecs[2][0] + p1End[0], vecs[2][1] + p1End[1])
    p3End = (vecs[3][0] + p2End[0], vecs[3][1] + p2End[1])
    
    pygame.draw.line(window, red, origin, p0End, 2)
    pygame.draw.line(window, green, p0End, p1End, 2)
    pygame.draw.line(window, blue, p1End, p2End, 2)
    pygame.draw.line(window, black, p2End, p3End, 2)

class Point:
    def __init__(self, origin, x, y):
        self.origin = origin
        self.x = x
        self.y = y
        self.radius = 10
        self.move = False

    def moveable(self, mouse, mousepos):
        if mouse[0]:
            rect = pygame.Rect(self.x - self.radius, self.y - self.radius, self.radius * 2, self.radius * 2)
            if rect.collidepoint(mousepos):
                self.move = True

        if not mouse[0]:
            self.move = False

        if self.move:
            self.x, self.y = mousepos

class Curve:
    def __init__(self, origin):
        self.origin = origin
        self.p0 = Point(origin, origin[0] - 120, origin[1] + 30)
        self.p1 = Point(origin, origin[0] + 120, origin[1] + 30)
        self.p2 = Point(origin, origin[0] - 110, origin[1] - 120)
        self.p3 = Point(origin, origin[0] + 110, origin[1] - 120)

    def moveable(self, mouse, mousepos):
        self.p0.moveable(mouse, mousepos)
        self.p1.moveable(mouse, mousepos)
        self.p2.moveable(mouse, mousepos)
        self.p3.moveable(mouse, mousepos)
        ps = (self.p0, self.p1, self.p2, self.p3)
        for i in range(4):
            if ps[i].move:
                for j in range(4):
                    if i != j:
                        ps[j].move = False

    def draw(self, window):
        drawPoint(window, self.p0, red)
        drawPoint(window, self.p1, green)
        drawPoint(window, self.p2, blue)
        drawPoint(window, self.p3, black)

        pygame.draw.line(window, red, (self.p0.x, self.p0.y), (self.p1.x, self.p1.y), 3)
        pygame.draw.line(window, green, (self.p1.x, self.p1.y), (self.p2.x, self.p2.y), 3)
        pygame.draw.line(window, blue, (self.p2.x, self.p2.y), (self.p3.x, self.p3.y), 3)

    def getControlVectors(self):
        findVector = lambda p : (p.x - self.origin[0], p.y - self.origin[1])
        return (findVector(self.p0), findVector(self.p1), findVector(self.p2), findVector(self.p3))

    def getScaledControlVectors(self, pVals):
        vectors = self.getControlVectors()
        scaleVector = lambda x : (vectors[x][0] *  pVals[x], vectors[x][1] *  pVals[x])
        return (scaleVector(0), scaleVector(1), scaleVector(2), scaleVector(3))

    def getPointOnCurve(self, pVals):
        vectors = self.getScaledControlVectors(pVals)
        addVector = lambda v1, v2 : (v1[0] + v2[0], v1[1] + v2[1])
        return addVector(self.origin, addVector(addVector(vectors[0], vectors[1]), addVector(vectors[2], vectors[3])))

    def getControlPoints(self):
        return (self.p0, self.p1, self.p2, self.p3)

        
curve = Curve((700, 500))   

scale = 380
window = pygame.display.set_mode((1200, 800))
clock = pygame.time.Clock()

while True:
    clock.tick(60)
    events = pygame.event.get()    
    for event in events:
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    mouse = pygame.mouse.get_pressed()
    mousePos = pygame.mouse.get_pos()
    keys = pygame.key.get_pressed()

    curve.moveable(mouse, mousePos)
    pygame.display.set_caption(f"Bezier Curves - fps - {int(clock.get_fps())}")
    
    if keys[pygame.K_w]: _t += 0.005
    if keys[pygame.K_q]: _t -= 0.005
    _t = min(1, max(_t, 0))

    window.fill((255, 255, 255))
    pygame.draw.rect(window, (0, 0, 0), (5, 50, 380, 380), 1)

    t = 0
    while t <= 1:
        cp0, cp1, cp2, cp3 = getPs(t)
        drawCurvePixel(window, cp0, t, red, scale)
        drawCurvePixel(window, cp1, t, green, scale)
        drawCurvePixel(window, cp2, t, blue, scale)
        drawCurvePixel(window, cp3, t, black, scale)

        pointAtTValue = curve.getPointOnCurve((cp0, cp1, cp2, cp3))
        pygame.draw.circle(window, (255, 0, 255), pointAtTValue, 3)

        t += 0.01
        
    cp0, cp1, cp2, cp3 = getPs(_t)
    drawPointOnCurve(window, cp0, _t, red, scale)
    drawPointOnCurve(window, cp1, _t, green, scale)
    drawPointOnCurve(window, cp2, _t, blue, scale)
    drawPointOnCurve(window, cp3, _t, black, scale)

    pygame.draw.rect(window, red, (5, 450, cp0 * 380, 50))
    pygame.draw.rect(window, green, (5, 510, cp1 * 380, 50))
    pygame.draw.rect(window, blue, (5, 570, cp2 * 380, 50))
    pygame.draw.rect(window, black, (5, 630, cp3 * 380, 50))

    curve.draw(window)
    pygame.draw.circle(window, black, curve.origin, 5, 1)
    pVals = (cp0, cp1, cp2, cp3)
    drawVectorsAdded(window, curve.origin, curve.getScaledControlVectors(pVals))
    pointAtTValue = curve.getPointOnCurve(pVals)
    pygame.draw.circle(window, (255, 0, 255), pointAtTValue, 10)

    val = str(_t)[0:5]
    tvalue = font.render(f"t = {val}", True, black)
    window.blit(tvalue, (10, 10))
    
    pygame.display.flip()

In the part where the curve is drawn

 t = 0
    while t <= 1:
        cp0, cp1, cp2, cp3 = getPs(t)
        drawCurvePixel(window, cp0, t, red, scale)
        drawCurvePixel(window, cp1, t, green, scale)
        drawCurvePixel(window, cp2, t, blue, scale)
        drawCurvePixel(window, cp3, t, black, scale)

        pointAtTValue = curve.getPointOnCurve((cp0, cp1, cp2, cp3))
        pygame.draw.circle(window, (255, 0, 255), pointAtTValue, 3)

        t += 0.01

If t is incremented by a small value, it will redraw same pixels more than once and bigger values will leave gaps between pixels. Is there a way to find an ideal increment size that does not redraw pixels but doesn't leave gaps either depending on the curve?

Pixel gaps:

enter image description here

Redrawing same pixels:

enter image description here


Solution

  • Concatenate the points with a line segments with pygame.draw.lines:

    curve_pts = []
    t = 0
    while t <= 1:
        cp0, cp1, cp2, cp3 = getPs(t)
        // [...]
    
        curve_pts.append(curve.getPointOnCurve((cp0, cp1, cp2, cp3)))
        t += 0.01
    
    pygame.draw.lines(window, (255, 0, 255), False, curve_pts, 3)