How to rotate a square around x-axis in a 3D space - python

So i have been trying to learn how 3D rendering works. I tried write a script with the goal to rotate a flat (2D) square in 3D space. I started by defining a square in a normalised space (-1, 1). Note that only x and y is normalised.
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
s = 1
p1 = Vec3(-s, -s, -s)
p2 = Vec3(s, -s, -s)
p3 = Vec3(s, s, -s)
p4 = Vec3(-s, s, -s)
Then translated the points into the screen:
p1.z += 6
p2.z += 6
p3.z += 6
p4.z += 6
Everything after this is done inside the application loop. I scaled the points into the the screen with projection applied using the function:
class Transform:
# IT TRANSFORMS THE X AND Y FROM NORMALISED SPACE TO SCREEN SPACE WITH PROJECTION APPLIED
def worldSpaceTransform(self, vec3, w, h):
if vec3.z == 0:
vec3.z = 0.001
zInverse = 1/ vec3.z
xTransformed = ((vec3.x * zInverse) + 1) * (w/2)
yTransformed = ((-vec3.y * zInverse) + 1) * (h/2)
xTransformed = str(xTransformed)[:6]
yTransformed = str(yTransformed)[:6]
return Vec2(float(xTransformed), float(yTransformed))
like this:
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
point1 = transform.worldSpaceTransform(p1, SCREENWIDTH, SCREENHEIGHT)
point2 = transform.worldSpaceTransform(p2, SCREENWIDTH, SCREENHEIGHT)
point3 = transform.worldSpaceTransform(p3, SCREENWIDTH, SCREENHEIGHT)
point4 = transform.worldSpaceTransform(p4, SCREENWIDTH, SCREENHEIGHT)
and drew the points:
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = ((point1.x, point1.y), (point2.x, point2.y),
(point2.x, point2.y), (point3.x, point3.y),
(point3.x, point3.y), (point4.x, point4.y),
(point4.x, point4.y), (point1.x, point1.y))
pygame.draw.lines(D, (0, 0, 0), False, points)
Everything so far works (i think) because it draws a square, as it's supposed to.
Now the rotation. I tried rotation for all the axis and none of them work, but for the sake of being specific, i will talk about the x-axis. The following is the rotation class. I copied the rotation matrices from wikipedia. I am not completely sure about how they work so i also dont know if it is compatible with the system i described above.
def multVecMatrix(vec3, mat3):
# MULTIPLIES A Vec3 OBJECT WITH Mat3 OBJECT AND RETURNS A NEW Vec3 ?
x = vec3.x * mat3.matrix[0][0] + vec3.y * mat3.matrix[0][1] + vec3.z * mat3.matrix[0][2]
y = vec3.x * mat3.matrix[1][0] + vec3.y * mat3.matrix[1][1] + vec3.z * mat3.matrix[1][2]
z = vec3.x * mat3.matrix[2][0] + vec3.y * mat3.matrix[2][1] + vec3.z * mat3.matrix[2][2]
return Vec3(x, y, z)
class Rotation:
def rotateX(self, theta):
# ROTATION MATRIX IN X AXIS ??
sinTheta = sin(theta)
cosTheta = cos(theta)
m = Mat3()
m.matrix = [[1, 0, 0],
[0, cosTheta, sinTheta],
[0, -sinTheta, cosTheta]]
return m
def rotate(self, vec3, theta, axis=None):
# ROTATES A Vec3 BY GIVEN THETA AND AXIS ??
if axis == "x":
return multVecMatrix(vec3, self.rotateX(theta))
if axis == "y":
return multVecMatrix(vec3, self.rotateY(theta))
if axis == "z":
return multVecMatrix(vec3, self.rotateZ(theta))
And it is called like this after filling the screen white and before scaling the points from normalised space to screen space.
# screen is filled with white color
# ROTATING THE POINTS AROUND X AXIS ?????
p1.x = rotation.rotate(p1, thetax, axis='x').x
p1.y = rotation.rotate(p1, thetay, axis='x').y
p1.z = rotation.rotate(p1, thetax, axis='x').z
p2.x = rotation.rotate(p2, thetax, axis='x').x
p2.y = rotation.rotate(p2, thetay, axis='x').y
p2.z = rotation.rotate(p2, thetax, axis='x').z
p3.x = rotation.rotate(p3, thetax, axis='x').x
p3.y = rotation.rotate(p3, thetay, axis='x').y
p3.z = rotation.rotate(p3, thetax, axis='x').z
p4.x = rotation.rotate(p4, thetax, axis='x').x
p4.y = rotation.rotate(p4, thetay, axis='x').y
p4.z = rotation.rotate(p4, thetax, axis='x').z
# then the points are translated into world space
After the rotation is applied, it looks like it is moving and circling the x-axis but not rotating. I want it to rotate while staying where it is. What am i doing wrong?
Complete copy-and-paste code for reference:
import pygame
from math import sin, cos, radians
pygame.init()
### PYGAME STUFF ######################################
SCREENWIDTH = 600
SCREENHEIGHT = 600
D = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("PRESS SPACE TO ROTATE AROUND X")
######### MATH FUNCTIONS AND CLASSES ####################
class Mat3:
# 3X3 MATRIX INITIALIZED WITH ALL 0's
def __init__(self):
self.matrix = [[0 for i in range(3)],
[0 for i in range(3)],
[0 for i in range(3)]]
class Vec2:
# 2D VECTOR
def __init__(self, x, y):
self.x = x
self.y = y
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def multVecMatrix(vec3, mat3):
# MULTIPLIES A Vec3 OBJECT WITH Mat3 OBJECT AND RETURNS A NEW Vec3
x = vec3.x * mat3.matrix[0][0] + vec3.y * mat3.matrix[0][1] + vec3.z * mat3.matrix[0][2]
y = vec3.x * mat3.matrix[1][0] + vec3.y * mat3.matrix[1][1] + vec3.z * mat3.matrix[1][2]
z = vec3.x * mat3.matrix[2][0] + vec3.y * mat3.matrix[2][1] + vec3.z * mat3.matrix[1][2]
return Vec3(x, y, z)
class Transform:
# IT TRANSFORMS THE X AND Y FROM NORMALIZED SPACE TO SCREEN SPACE WITH PROJECTION APPLIED
def worldSpaceTransform(self, vec3, w, h):
if vec3.z == 0:
vec3.z = 0.001
zInverse = 1/ vec3.z
xTransformed = ((vec3.x * zInverse) + 1) * (w/2)
yTransformed = ((-vec3.y * zInverse) + 1) * (h/2)
xTransformed = str(xTransformed)[:6]
yTransformed = str(yTransformed)[:6]
return Vec2(float(xTransformed), float(yTransformed))
class Rotation:
def rotateX(self, theta):
# ROTATION MATRIX IN X AXIS
sinTheta = sin(theta)
cosTheta = cos(theta)
m = Mat3()
m.matrix = [[1, 0, 0],
[0, cosTheta, sinTheta],
[0, -sinTheta, cosTheta]]
return m
def rotate(self, vec3, theta, axis=None):
# ROTATES A Vec3 BY GIVEN THETA AND AXIS
if axis == "x":
return multVecMatrix(vec3, self.rotateX(theta))
if axis == "y":
return multVecMatrix(vec3, self.rotateY(theta))
if axis == "z":
return multVecMatrix(vec3, self.rotateZ(theta))
transform = Transform()
rotation = Rotation()
# ASSIGNING 4 Vec3's FOR 4 SIDES OF SQUARE IN NORMALIZED SPACE
s = 1
p1 = Vec3(-s, -s, -s)
p2 = Vec3(s, -s, -s)
p3 = Vec3(s, s, -s)
p4 = Vec3(-s, s, -s)
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
p1.z += 6
p2.z += 6
p3.z += 6
p4.z += 6
# ASSIGNING THE ROTATION ANGLES
thetax = 0
# APPLICATION LOOP
while True:
pygame.event.get()
D.fill((255, 255, 255))
# ROTATING THE POINTS AROUND X AXIS
p1.x = rotation.rotate(p1, thetax, axis='x').x
p1.y = rotation.rotate(p1, thetax, axis='x').y
p1.z = rotation.rotate(p1, thetax, axis='x').z
p2.x = rotation.rotate(p2, thetax, axis='x').x
p2.y = rotation.rotate(p2, thetax, axis='x').y
p2.z = rotation.rotate(p2, thetax, axis='x').z
p3.x = rotation.rotate(p3, thetax, axis='x').x
p3.y = rotation.rotate(p3, thetax, axis='x').y
p3.z = rotation.rotate(p3, thetax, axis='x').z
p4.x = rotation.rotate(p4, thetax, axis='x').x
p4.y = rotation.rotate(p4, thetax, axis='x').y
p4.z = rotation.rotate(p4, thetax, axis='x').z
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
point1 = transform.worldSpaceTransform(p1, SCREENWIDTH, SCREENHEIGHT)
point2 = transform.worldSpaceTransform(p2, SCREENWIDTH, SCREENHEIGHT)
point3 = transform.worldSpaceTransform(p3, SCREENWIDTH, SCREENHEIGHT)
point4 = transform.worldSpaceTransform(p4, SCREENWIDTH, SCREENHEIGHT)
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = ((point1.x, point1.y), (point2.x, point2.y),
(point2.x, point2.y), (point3.x, point3.y),
(point3.x, point3.y), (point4.x, point4.y),
(point4.x, point4.y), (point1.x, point1.y))
keys = pygame.key.get_pressed()
# ROTATE X ?
if keys[pygame.K_SPACE]:
thetax -= 0.005
pygame.draw.lines(D, (0, 0, 0), False, points)
pygame.display.flip()

It is not necessary to rotate each component of a vector separately. If you do
p1.x = rotation.rotate(p1, thetax, axis='x').x
then the x component of p1 has changed and the p1 which is passed to the next instruction is different
p1.y = rotation.rotate(p1, thetay, axis='x').y
It is sufficient to rotate the entire vertices once:
p1 = rotation.rotate(p1, thetax, axis='x')
p2 = rotation.rotate(p2, thetax, axis='x')
p3 = rotation.rotate(p3, thetax, axis='x')
p4 = rotation.rotate(p4, thetax, axis='x')
When you multiply a vector by a rotation matrix, then the vector is rotated a round (0, 0, 0). You have to do the translation after the rotation.
Add a +-operator to the Vec3 class:
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __add__(a, b):
return Vec3(a.x+b.x, a.y+b.y, a.z+b.z)
Never change the original vertex coordinates p1, p2, p3 and p4. Compute the rotation and then the translation:
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
#p1.z += 6 <--- DELETE
#p2.z += 6
#p3.z += 6
#p4.z += 6
transVec = Vec3(0, 0, 6)
# [...]
while run:
# ROTATING THE POINTS AROUND X AXIS
point1 = rotation.rotate(p1, thetax, axis='x')
# [...]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
point1 = point1 + transVec
# [...]
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
point1 = transform.worldSpaceTransform(point1, SCREENWIDTH, SCREENHEIGHT)
# [...]
I recommend to organize the vertex coordinates in lists:
# ASSIGNING 4 Vec3's FOR 4 SIDES OF SQUARE IN NORMALIZED SPACE
s = 1
modelPoints = [Vec3(-s, -s, -s), Vec3(s, -s, -s), Vec3(s, s, -s), Vec3(-s, s, -s)]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
transVec = Vec3(0, 0, 6)
# ASSIGNING THE ROTATION ANGLES
thetax = 0
# APPLICATION LOOP
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
D.fill((255, 255, 255))
# ROTATING THE POINTS AROUND X AXIS
points = [rotation.rotate(pt, thetax, axis='x') for pt in modelPoints]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
points = [pt + transVec for pt in points]
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
points = [transform.worldSpaceTransform(pt, SCREENWIDTH, SCREENHEIGHT) for pt in points]
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = [(pt.x, pt.y) for pt in points]
See the complete example:
import pygame
from math import sin, cos, radians
pygame.init()
### PYGAME STUFF ######################################
SCREENWIDTH = 600
SCREENHEIGHT = 600
D = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("PRESS SPACE TO ROTATE AROUND X")
######### MATH FUNCTIONS AND CLASSES ####################
class Mat3:
# 3X3 MATRIX INITIALIZED WITH ALL 0's
def __init__(self):
self.matrix = [[0 for i in range(3)],
[0 for i in range(3)],
[0 for i in range(3)]]
class Vec2:
# 2D VECTOR
def __init__(self, x, y):
self.x = x
self.y = y
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __add__(a, b):
return Vec3(a.x+b.x, a.y+b.y, a.z+b.z)
def multVecMatrix(vec3, mat3):
# MULTIPLIES A Vec3 OBJECT WITH Mat3 OBJECT AND RETURNS A NEW Vec3
x = vec3.x * mat3.matrix[0][0] + vec3.y * mat3.matrix[0][1] + vec3.z * mat3.matrix[0][2]
y = vec3.x * mat3.matrix[1][0] + vec3.y * mat3.matrix[1][1] + vec3.z * mat3.matrix[1][2]
z = vec3.x * mat3.matrix[2][0] + vec3.y * mat3.matrix[2][1] + vec3.z * mat3.matrix[2][2]
return Vec3(x, y, z)
class Transform:
# IT TRANSFORMS THE X AND Y FROM NORMALIZED SPACE TO SCREEN SPACE WITH PROJECTION APPLIED
def worldSpaceTransform(self, vec3, w, h):
if vec3.z == 0:
vec3.z = 0.001
zInverse = 1/ vec3.z
xTransformed = ((vec3.x * zInverse) + 1) * (w/2)
yTransformed = ((-vec3.y * zInverse) + 1) * (h/2)
xTransformed = str(xTransformed)[:6]
yTransformed = str(yTransformed)[:6]
return Vec2(float(xTransformed), float(yTransformed))
class Rotation:
def rotateX(self, theta):
# ROTATION MATRIX IN X AXIS
sinTheta = sin(theta)
cosTheta = cos(theta)
m = Mat3()
m.matrix = [[1, 0, 0],
[0, cosTheta, sinTheta],
[0, -sinTheta, cosTheta]]
return m
def rotate(self, vec3, theta, axis=None):
# ROTATES A Vec3 BY GIVEN THETA AND AXIS
if axis == "x":
return multVecMatrix(vec3, self.rotateX(theta))
if axis == "y":
return multVecMatrix(vec3, self.rotateY(theta))
if axis == "z":
return multVecMatrix(vec3, self.rotateZ(theta))
transform = Transform()
rotation = Rotation()
# ASSIGNING 4 Vec3's FOR 4 SIDES OF SQUARE IN NORMALIZED SPACE
s = 1
modelPoints = [Vec3(-s, -s, -s), Vec3(s, -s, -s), Vec3(s, s, -s), Vec3(-s, s, -s)]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
transVec = Vec3(0, 0, 6)
# ASSIGNING THE ROTATION ANGLES
thetax = 0
# APPLICATION LOOP
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
D.fill((255, 255, 255))
# ROTATING THE POINTS AROUND X AXIS
points = [rotation.rotate(pt, thetax, axis='x') for pt in modelPoints]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
points = [pt + transVec for pt in points]
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
points = [transform.worldSpaceTransform(pt, SCREENWIDTH, SCREENHEIGHT) for pt in points]
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = [(pt.x, pt.y) for pt in points]
keys = pygame.key.get_pressed()
# ROTATE X ?
if keys[pygame.K_SPACE]:
thetax -= 0.005
pygame.draw.lines(D, (0, 0, 0), True, points)
pygame.display.flip()

Related

How can I connect two points with a series of circles?

I am trying to make realistic water in pygame:
This is till now my code:
from random import randint
import pygame
WIDTH = 700
HEIGHT = 500
win = pygame.display.set_mode((WIDTH, HEIGHT))
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
AQUA = 'aqua'
RADIUS = 1
x, y = 0, HEIGHT//2
K = 1
FORCE = 100
VELOCITY = 0.5
run = True
class Molecule:
def __init__(self, x, y, radius, force, k):
self.x = x
self.y = y
self.radius = radius
self.force = force
self.k = k
self.max_amplitude = y + force/k
self.min_amplitude = y - force/k
self.up = False
self.down = True
self.restore = False
def draw(self, win):
pygame.draw.circle(win, BLACK, (self.x, self.y), self.radius)
def oscillate(self):
if self.y <= self.max_amplitude and self.down == True:
self.y += VELOCITY
if self.y == self.max_amplitude or self.up:
self.up = True
self.down = False
self.y -= VELOCITY
if self.y == self.min_amplitude:
self.up = False
self.down = True
molecules = []
for i in range(100):
FORCE = randint(10, 20)
molecules.append(Molecule(x, y, RADIUS, FORCE, K))
x += 10
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
win.fill(WHITE)
for molecule in molecules:
molecule.draw(win)
molecule.oscillate()
for i in range(len(molecules)):
try:
pygame.draw.line(win, BLACK, (molecules[i].x, molecules[i].y), (molecules[i+1].x, molecules[i+1].y))
pygame.draw.line(win, AQUA, (molecules[i].x, molecules[i].y), (molecules[i+1].x, HEIGHT))
except:
pass
pygame.display.flip()
pygame.quit()
But as may expected the water curve is not smooth:
Look at it:
Sample Img1
I want to connect the two randomly added wave points using a set of circles not line like in this one so that a smooth curve could occur.
And in this way i could add the water color to it such that it will draw aqua lines or my desired color line from the point to the end of screen and all this will end up with smooth water flowing simulation.
Now the question is how could i make the points connect together smoothly into a smooth curve by drawing point circles at relative points?
I suggest sticking the segments with a Bézier curves. Bézier curves can be drawn with pygame.gfxdraw.bezier
Calculate the slopes of the tangents to the points along the wavy waterline:
ts = []
for i in range(len(molecules)):
pa = molecules[max(0, i-1)]
pb = molecules[min(len(molecules)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
Use the the tangents to define 4 control points for each segment and draw the curve with pygame.gfxdraw.bezier:
for i in range(len(molecules)-1):
p0 = molecules[i].x, molecules[i].y
p3 = molecules[i+1].x, molecules[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pygame.gfxdraw.bezier(win, [p0, p1, p2, p3], 4, BLACK)
Complete example:
from random import randint
import pygame
import pygame.gfxdraw
WIDTH = 700
HEIGHT = 500
win = pygame.display.set_mode((WIDTH, HEIGHT))
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
AQUA = 'aqua'
RADIUS = 1
x, y = 0, HEIGHT//2
K = 1
FORCE = 100
VELOCITY = 0.5
class Molecule:
def __init__(self, x, y, radius, force, k):
self.x = x
self.y = y
self.radius = radius
self.force = force
self.k = k
self.max_amplitude = y + force/k
self.min_amplitude = y - force/k
self.up = False
self.down = True
self.restore = False
def draw(self, win):
pygame.draw.circle(win, BLACK, (self.x, self.y), self.radius)
def oscillate(self):
if self.y <= self.max_amplitude and self.down == True:
self.y += VELOCITY
if self.y == self.max_amplitude or self.up:
self.up = True
self.down = False
self.y -= VELOCITY
if self.y == self.min_amplitude:
self.up = False
self.down = True
molecules = []
for i in range(50):
FORCE = randint(10, 20)
molecules.append(Molecule(x, y, RADIUS, FORCE, K))
x += 20
clock = pygame.time.Clock()
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
win.fill(WHITE)
for molecule in molecules:
molecule.draw(win)
molecule.oscillate()
ts = []
for i in range(len(molecules)):
pa = molecules[max(0, i-1)]
pb = molecules[min(len(molecules)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
for i in range(len(molecules)-1):
p0 = molecules[i].x, molecules[i].y
p3 = molecules[i+1].x, molecules[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pygame.gfxdraw.bezier(win, [p0, p1, p2, p3], 4, BLACK)
for i in range(len(molecules)-1):
pygame.draw.line(win, AQUA, (molecules[i].x, molecules[i].y), (molecules[i].x, HEIGHT))
pygame.display.flip()
pygame.quit()
If you want to "fill" the water, you must calculate the points along the Bézier line and draw a filled polygon. How to calculate a Bézier curve is explained in Trying to make a Bezier Curve on PyGame library How Can I Make a Thicker Bezier in Pygame? and "X". You can use the following function:
def ptOnCurve(b, t):
q = b.copy()
for k in range(1, len(b)):
for i in range(len(b) - k):
q[i] = (1-t) * q[i][0] + t * q[i+1][0], (1-t) * q[i][1] + t * q[i+1][1]
return round(q[0][0]), round(q[0][1])
def bezier(b, samples):
return [ptOnCurve(b, i/samples) for i in range(samples+1)]
Use the bezier to stitch the wavy water polygon:
ts = []
for i in range(len(molecules)):
pa = molecules[max(0, i-1)]
pb = molecules[min(len(molecules)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
pts = [(WIDTH, HEIGHT), (0, HEIGHT)]
for i in range(len(molecules)-1):
p0 = molecules[i].x, molecules[i].y
p3 = molecules[i+1].x, molecules[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pts += bezier([p0, p1, p2, p3], 4)
Draw the polygon with pygame.draw.polygon():
pygame.draw.polygon(win, AQUA, pts)
Complete example:
from random import randint
import pygame
class Node:
def __init__(self, x, y, force, k, v):
self.x = x
self.y = y
self.y0 = y
self.force = force
self.k = k
self.v = v
self.direction = 1
def oscillate(self):
self.y += self.v * self.direction
if self.y0 - self.force / self.k > self.y or self.y0 + self.force / self.k < self.y:
self.direction *= -1
def draw(self, surf):
pygame.draw.circle(surf, "black", (self.x, self.y), 3)
window = pygame.display.set_mode((700, 500))
clock = pygame.time.Clock()
width, height = window.get_size()
no_of_nodes = 25
dx = width / no_of_nodes
nodes = [Node(i*dx, height//2, randint(15, 30), 1, 0.5) for i in range(no_of_nodes+1)]
def ptOnCurve(b, t):
q = b.copy()
for k in range(1, len(b)):
for i in range(len(b) - k):
q[i] = (1-t) * q[i][0] + t * q[i+1][0], (1-t) * q[i][1] + t * q[i+1][1]
return round(q[0][0]), round(q[0][1])
def bezier(b, samples):
return [ptOnCurve(b, i/samples) for i in range(samples+1)]
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
for molecule in nodes:
molecule.oscillate()
ts = []
for i in range(len(nodes)):
pa = nodes[max(0, i-1)]
pb = nodes[min(len(nodes)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
pts = [(width, height), (0, height)]
for i in range(len(nodes)-1):
p0 = nodes[i].x, nodes[i].y
p3 = nodes[i+1].x, nodes[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pts += bezier([p0, p1, p2, p3], 4)
window.fill("white")
pygame.draw.polygon(window, 'aqua', pts)
for molecule in nodes:
molecule.draw(window)
pygame.display.flip()
pygame.quit()
exit()

finding velx, and vely knowing only final x, y and g [duplicate]

So I created this parabola class which can be instantiated with 3 parameters (a, b and c) or with 3 points belonging to the parabola. The punti() function returns all the points belonging to the parabola in a range defined by n and m. Here's the code (Most of this is in Italian, sorry):
class Parabola:
def __init__(self, tipo=0, *params):
'''
Il tipo è 0 per costruire la parabola con a, b, c; 1 per costruire la parabola con
tre punti per la quale passa
'''
if tipo == 0:
self.__a = params[0]
self.__b = params[1]
self.__c = params[2]
self.__delta = self.__b ** 2 - (4 * self.__a * self.__c)
elif tipo == 1:
matrix_a = np.array([
[params[0][0]**2, params[0][0], 1],
[params[1][0]**2, params[1][0], 1],
[params[2][0]**2, params[2][0], 1]
])
matrix_b = np.array([params[0][1], params[1][1], params[2][1]])
matrix_c = np.linalg.solve(matrix_a, matrix_b)
self.__a = round(matrix_c[0], 2)
self.__b = round(matrix_c[1], 2)
self.__c = round(matrix_c[2], 2)
self.__delta = self.__b ** 2 - (4 * self.__a * self.__c)
def trovaY(self, x):
y = self.__a * x ** 2 + self.__b * x + self.__c
return y
def punti(self, n, m, step=1):
output = []
for x in range(int(min(n, m)), int(max(n, m)) + 1, step):
output.append((x, self.trovaY(x)))
return output
Now my little game is about shooting targets with a bow and i have to use the parabola for the trajectory and it passes by 3 points:
The player center
A point with the cursor's x and player's y
A point in the middle with the cursors's y
The trajectory is represented by a black line but it clearly doesn't work and I can't understand why. Here's the code of the game (Don't mind about the bow's rotation, I still have to make it function properly):
import os
import sys
import pygame
from random import randint
sys.path.insert(
1, __file__.replace("pygame-prototype\\" + os.path.basename(__file__), "coniche\\")
)
import parabola
# Initialization
pygame.init()
WIDTH, HEIGHT = 1024, 576
screen = pygame.display.set_mode((WIDTH, HEIGHT))
# Function to rotate without losing quality
def rot_from_zero(surface, angle):
rotated_surface = pygame.transform.rotozoom(surface, angle, 1)
rotated_rect = rotated_surface.get_rect()
return rotated_surface, rotated_rect
# Function to map a range of values to another
def map_range(value, leftMin, leftMax, rightMin, rightMax):
# Figure out how 'wide' each range is
leftSpan = leftMax - leftMin
rightSpan = rightMax - rightMin
# Convert the left range into a 0-1 range (float)
valueScaled = float(value - leftMin) / float(leftSpan)
# Convert the 0-1 range into a value in the right range.
return rightMin + (valueScaled * rightSpan)
# Player class
class Player:
def __init__(self, x, y, width=64, height=64):
self.rect = pygame.Rect(x, y, width, height)
self.dirx = 0
self.diry = 0
def draw(self):
rectangle = pygame.draw.rect(screen, (255, 0, 0), self.rect)
# Target class
class Target:
def __init__(self, x, y, acceleration=0.25):
self.x, self.y = x, y
self.image = pygame.image.load(
__file__.replace(os.path.basename(__file__), "target.png")
)
self.speed = 0
self.acceleration = acceleration
def draw(self):
screen.blit(self.image, (self.x, self.y))
def update(self):
self.speed -= self.acceleration
self.x += int(self.speed)
if self.speed < -1:
self.speed = 0
player = Player(64, HEIGHT - 128)
# Targets init
targets = []
targets_spawn_time = 3000
previous_ticks = pygame.time.get_ticks()
# Ground animation init
ground_frames = []
for i in os.listdir(__file__.replace(os.path.basename(__file__), "ground_frames")):
ground_frames.append(
pygame.image.load(
__file__.replace(os.path.basename(__file__), "ground_frames\\" + i)
)
) # Load all ground frames
ground_frame_counter = 0 # Keep track of the current ground frame
frame_counter = 0
# Bow
bow = pygame.image.load(__file__.replace(os.path.basename(__file__), "bow.png"))
angle = 0
while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Spawning the targets
current_ticks = pygame.time.get_ticks()
if current_ticks - previous_ticks >= targets_spawn_time:
targets.append(Target(WIDTH, randint(0, HEIGHT - 110)))
previous_ticks = current_ticks
screen.fill((101, 203, 214))
player.draw()
for i, e in list(enumerate(targets))[::-1]:
e.draw()
e.update()
if e.x <= -e.image.get_rect().width:
del targets[i]
# Calculating the angle of the bow
mouse_pos = pygame.Vector2(pygame.mouse.get_pos())
angle = map_range(mouse_pos.x, 0, WIDTH, 90, 0)
# Rotate the bow
rotated_bow, rotated_bow_rect = rot_from_zero(bow, angle)
rotated_bow_rect.center = player.rect.center
screen.blit(rotated_bow, rotated_bow_rect)
# Animate the ground
if frame_counter % 24 == 0:
ground_frame_counter += 1
if ground_frame_counter >= len(ground_frames):
ground_frame_counter = 0
for i in range(round(WIDTH / ground_frames[ground_frame_counter].get_rect().width)):
screen.blit(
ground_frames[ground_frame_counter],
(
ground_frames[ground_frame_counter].get_rect().width * i,
HEIGHT - ground_frames[ground_frame_counter].get_rect().height,
),
)
# Calculating the trajectory
mouse_pos.x = (
mouse_pos.x if mouse_pos.x != rotated_bow_rect.centerx else mouse_pos.x + 1
)
# print(mouse_pos, rotated_bow_rect.center)
v_x = rotated_bow_rect.centerx + ((mouse_pos.x - rotated_bow_rect.centerx) / 2)
trajectory_parabola = parabola.Parabola(
1,
rotated_bow_rect.center,
(mouse_pos.x, rotated_bow_rect.centery),
(v_x, mouse_pos.y),
)
trajectory = [(i[0], int(i[1])) for i in trajectory_parabola.punti(0, WIDTH)]
pygame.draw.lines(screen, (0, 0, 0), False, trajectory)
pygame.draw.ellipse(
screen, (128, 128, 128), pygame.Rect(v_x - 15, mouse_pos.y - 15, 30, 30)
)
pygame.draw.ellipse(
screen,
(128, 128, 128),
pygame.Rect(mouse_pos.x - 15, rotated_bow_rect.centery - 15, 30, 30),
)
pygame.display.update()
if frame_counter == 120:
for i in trajectory:
print(i)
frame_counter += 1
You can run all of this and understand what's wrong with it, help?
You round the values of a, b and c to 2 decimal places. This is too inaccurate for this application:
self.__a = round(matrix_c[0], 2)
self.__b = round(matrix_c[1], 2)
self.__c = round(matrix_c[2], 2)
self.__a = matrix_c[0]
self.__b = matrix_c[1]
self.__c = matrix_c[2]
Similar to answer above... rounding is the issue here. This is magnified when the scale of the coordinates gets bigger.
However, disagree with other solution: It does not matter what order you pass the coordinates into your parabola construction. Any order works fine. points are points.
Here is a pic of your original parabola function "drooping" because of rounding error:
p1 = (0, 10) # left
p2 = (100, 10) # right
p3 = (50, 100) # apex
p = Parabola(1, p3, p2, p1)
traj = p.punti(0, 100)
xs, ys = zip(*traj)
plt.scatter(xs, ys)
plt.plot([0, 100], [10, 10], color='r')
plt.show()

Misalignment of triangles drawn with tkinter canvas

I wrote this function that draw a grid of triangles:
def create_triangles(side_length):
result = []
half_width = int(side_length / 2)
# height = int(side_length * math.sqrt(3) / 2)
height = side_length
max_width = 15 * side_length
max_height = 10 * height
for i in range(0, max_height, height):
if (i / height) % 2 == 0:
for j in range(0, max_width-half_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-half_width, i+height/2, j, i-height/2, j+half_width)
else:
triangle = (i-height/2, j, i+height/2, j+half_width, i+height/2, j-half_width)
result.append(triangle)
else:
for j in range(half_width, max_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-2*half_width, i+height/2, j-half_width+2, i-height/2, j)
else:
triangle = (i-height/2, j-half_width, i+height/2, j, i+height/2, j-2*half_width)
result.append(triangle)
return result
The current output is this:
As you can see some triangles are misaligned but I don't understand why.
As mentioned in the comments, floating points give you incorrect results; You want to make sure that the shared points representing the vertices of two adjacent triangles are concurrent. A simple approach is to reduce the points coordinates to ints, and organize the calculations so errors do not add up.
In the following examples, the misalignment is corrected, every triangle on the canvas is represented by a polygon, and individually drawn; each triangle can therefore be referenced when moused over, or addressed via an index, or a mapping (not implemented).
import tkinter as tk
import math
WIDTH, HEIGHT = 500, 500
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def tile_with_triangles(canvas, side_length=50):
"""tiles the entire surface of the canvas with triangular polygons
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='red')
p2 = Point(-triangle_height, half_side) # flip the model triangle
for idx, x in enumerate(range(-triangle_height, WIDTH+triangle_height+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='blue')
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='cyan')
canvas.pack()
tile_with_triangles(canvas) #, side_length=10)
root.mainloop()
I added an active fill property that will change the colors of each triangle when you mouse over.

How to Create a Rainbow Triangle Tessellation

I'm attempting to create a triangle tessellation like the following in Python:
All I've gotten is Sierpensky's triangle. I assume it'd use some of the same code.
import turtle as t
import math
import colorsys
t.hideturtle()
t.speed(0)
t.tracer(0,0)
h = 0
def draw_tri(x,y,size):
global h
t.up()
t.goto(x,y)
t.seth(0)
t.down()
color = colorsys.hsv_to_rgb(h,1,1)
h += 0.1
t.color(color)
t.left(120)
t.fd(size)
t.left(120)
t.fd(size)
t.end_fill()
def draw_s(x,y,size,n):
if n == 0:
draw_tri(x,y,size)
return
draw_s(x,y,size/2,n-1)
draw_s(x+size/2,y,size/2,n-1)
draw_s(x+size/4,y+size*math.sqrt(3)/4,size/2,n-1)
draw_s(-300,-250,600,6)
t.update()
There are various approaches; the following example generates all line segments prior to directing the turtle to draw them on the canvas.
import turtle as t
import math
WIDTH, HEIGHT = 800, 800
OFFSET = -WIDTH // 2, -HEIGHT // 2
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def get_line_segments(side_length=50):
"""calculates the coordinates of all vertices
organizes them by line segment
stores the segments in a container and returns it
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
segments = []
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
segments += [[pa, pb], [pb, pc], [pc, pa]]
return segments
def draw_segment(segment):
p0, p1 = segment
p0, p1 = p0 + offset, p1 + offset
t.penup()
t.goto(p0)
t.pendown()
t.goto(p1)
def draw_tiling():
for segment in get_line_segments():
draw_segment(segment)
t.hideturtle()
t.speed(0)
t.tracer(0,0)
offset = Point(*OFFSET)
draw_tiling()
t.update()
t.exitonclick()
If you want to see how the tiling is traced, you can replace the following lines:
# t.hideturtle()
t.speed(1)
# t.tracer(0, 0)
and enlarge the canvas screen with your mouse to see the boundary of the tiling (I made it overlap the standard size of the window)
As #ReblochonMasque notes, there are multiple approaches to the problem. Here's one I worked out to use as little turtle code as possible to solve the problem:
from turtle import Screen, Turtle
TRIANGLE_SIDE = 60
TRIANGLE_HEIGHT = TRIANGLE_SIDE * 3 ** 0.5 / 2
CURSOR_SIZE = 20
screen = Screen()
width = TRIANGLE_SIDE * (screen.window_width() // TRIANGLE_SIDE)
height = TRIANGLE_HEIGHT * (screen.window_height() // TRIANGLE_HEIGHT)
diagonal = width + height
turtle = Turtle('square', visible=False)
turtle.shapesize(diagonal / CURSOR_SIZE, 1 / CURSOR_SIZE)
turtle.penup()
turtle.sety(height/2)
turtle.setheading(270)
turtle = turtle.clone()
turtle.setx(width/2)
turtle.setheading(210)
turtle = turtle.clone()
turtle.setx(-width/2)
turtle.setheading(330)
for _ in range(int(diagonal / TRIANGLE_HEIGHT)):
for turtle in screen.turtles():
turtle.forward(TRIANGLE_HEIGHT)
turtle.stamp()
screen.exitonclick()
It probably could use optimizing but it gets the job done. And it's fun to watch...

How to get the position of the point in Vispy 3d plot?

I am beginner of the VisPy.
All I want to do is:
Click the point then this point will change color and print the position (x,y,z) of this point.
But I can't find how to do this.
Here is my code.
import numpy as np
import sys
from vispy import app, visuals, scene
class Canvas(scene.SceneCanvas):
""" A simple test canvas for testing the EditLineVisual """
def __init__(self):
scene.SceneCanvas.__init__(self, keys='interactive',
size=(800, 800), show=True)
# # Create some initial points
self.unfreeze()
# Add a ViewBox to let the user zoom/rotate
self.view = self.central_widget.add_view()
self.view.camera = 'turntable'
self.view.camera.fov = 30
self.show()
self.selected_point = None
scene.visuals.GridLines(parent=self.view.scene)
self.freeze()
def on_mouse_press(self, event):
print(event.pos) # How to convert this pos to canvas position??
Scatter3D = scene.visuals.create_visual_node(visuals.MarkersVisual)
canvas = Canvas()
p1 = Scatter3D(parent=canvas.view.scene)
p1.set_gl_state('translucent', blend=True, depth_test=True)
# fake data
x = np.random.rand(100) * 10
y = np.random.rand(100) * 10
z = np.random.rand(100) * 10
# Draw it
point_list = [x, y, z]
point = np.array(point_list).transpose()
p1.set_data(point, symbol='o', size=6, edge_width=0.5, edge_color='blue')
if __name__ == "__main__":
if sys.flags.interactive != 1:
app.run()
I just had the same problem. I solved it by using the following code, hope this helps.
def on_mouse_press(self, event):
#1=left, 2=right , 3=middle button
if event.button == 1:
p2 = event.pos
norm = np.mean(self.view.camera._viewbox.size)
if self.view.camera._event_value is None or len(self.view.camera._event_value) == 2:
ev_val = self.view.camera.center
else:
ev_val = self.view.camera._event_value
dist = p2 / norm * self.view.camera._scale_factor
dist[1] *= -1
# Black magic part 1: turn 2D into 3D translations
dx, dy, dz = self.view.camera._dist_to_trans(dist)
# Black magic part 2: take up-vector and flipping into account
ff = self.view.camera._flip_factors
up, forward, right = self.view.camera._get_dim_vectors()
dx, dy, dz = right * dx + forward * dy + up * dz
dx, dy, dz = ff[0] * dx, ff[1] * dy, dz * ff[2]
c = ev_val
#shift by scale_factor half
sc_half = self.view.camera._scale_factor/2
point = c[0] + dx-sc_half, c[1] + dy-sc_half, c[2] + dz+sc_half
print("final point:", point[0], point[1], point[2])

Categories

Resources