8
\$\begingroup\$

This is my first self-guided programming project, and all the math used in the program is self-taught. I wanted to get a better understanding of how computer graphics worked, but I just feel more confused. Please tell me what I could do better in my next attempt programming something like this and how to learn more. Note: if you are running this code, it's not broken but takes about 40 seconds for the image to be drawn.

import math
import turtle

window = turtle.Screen()
window.title("My Window")
window.bgcolor(0.65, 0.65, 0.65)
window.tracer(0)
window.setup(width=1200, height=500)


turtle = turtle.Turtle()

turtle.hideturtle()
turtle.speed(0)
turtle.pencolor(1, 0, 0)
turtle.pensize(0.001)

light = [60, 60, -50]

sphere = [0, 0, 101]

floorLevel = 100

radius = 100

camera = [0, 0, -100]

pixel = [20, 20, 0]


def checkIntersections_sphere(camera, pixel, sphere, radius):
    global point_of_contact
    point_of_contact = []

    a = (
        (pixel[0] - camera[0]) ** 2
        + (pixel[1] - camera[1]) ** 2
        + (pixel[2] - camera[2]) ** 2
    )

    b = (-1 * 2) * (
        (pixel[0] - camera[0]) * (sphere[0] - camera[0])
        + (pixel[1] - camera[1]) * (sphere[1] - camera[1])
        + (sphere[2] - camera[2]) * (pixel[2] - camera[2])
    )

    c = (
        (sphere[0] - camera[0]) ** 2
        + (sphere[1] - camera[1]) ** 2
        + (sphere[2] - camera[2]) ** 2
        - radius**2
    )

    d = b**2 - 4 * a * c

    if d < 0:
        point_of_contact = []
        return False
    else:
        if d == 0:
            solution_1 = (-b - math.sqrt(d)) / (2 * a)
            point_of_contact = [
                camera[0] + solution_1 * (pixel[0] - camera[0]),
                camera[1] + solution_1 * (pixel[1] - camera[1]),
                camera[2] + solution_1 * (pixel[2] - camera[2]),
            ]
            return True
        else:
            solution_1 = (-b - math.sqrt(d)) / (2 * a)
            solution_2 = (-b + math.sqrt(d)) / (2 * a)

            point_of_contact1 = [
                camera[0] + solution_1 * (pixel[0] - camera[0]),
                camera[1] + solution_1 * (pixel[1] - camera[1]),
                camera[2] + solution_1 * (pixel[2] - camera[2]),
            ]
            point_of_contact2 = [
                camera[0] + solution_2 * (pixel[0] - camera[0]),
                camera[1] + solution_2 * (pixel[1] - camera[1]),
                camera[2] + solution_2 * (pixel[2] - camera[2]),
            ]

            distance1 = math.sqrt(
                (point_of_contact1[0] - camera[0]) ** 2
                + (point_of_contact1[1] - camera[1]) ** 2
                + (point_of_contact1[2] - camera[2]) ** 2
            )
            distance2 = math.sqrt(
                (point_of_contact2[0] - camera[0]) ** 2
                + (point_of_contact2[1] - camera[1]) ** 2
                + (point_of_contact2[2] - camera[2]) ** 2
            )

            if distance1 < distance2:
                point_of_contact = point_of_contact1
            else:
                point_of_contact = point_of_contact2
            return True


def color(pixel, diffrence_in_angle, spector):
    turtle.penup()
    turtle.pencolor(1 - diffrence_in_angle, 0 + spector, 0 + spector)
    turtle.goto(pixel[0], pixel[1])
    turtle.dot()
    turtle.pendown()
    turtle.penup()


def getColor(sphere, point_of_contact):
    global diffrence_in_angle
    global spector
    global spector_1
    spector = 0
    spector_1 = 0

    normal_vector = [
        point_of_contact[0] - sphere[0],
        point_of_contact[1] - sphere[1],
        point_of_contact[2] - sphere[2],
    ]

    light_vector = [
        light[0] - point_of_contact[0],
        light[1] - point_of_contact[1],
        light[2] - point_of_contact[2],
    ]

    step_1 = (
        (normal_vector[0] * light_vector[0])
        + (normal_vector[1] * light_vector[1])
        + (normal_vector[2] * light_vector[2])
    )

    step_2 = math.sqrt(
        normal_vector[0] ** 2 + normal_vector[1] ** 2 + normal_vector[2] ** 2
    )

    step_3 = math.sqrt(
        light_vector[0] ** 2 + light_vector[1] ** 2 + light_vector[2] ** 2
    )

    step_4 = (step_1) / (step_2 * step_3)

    diffrence_in_angle = (math.acos(step_4)) / 3.14

    spector = 1 - diffrence_in_angle**0.09333


def checkFloor(camera, pixel, floorLevel):
    global floor_contact
    if pixel[1] == 0:
        return False
    else:
        t = -1 * (camera[1] + floorLevel) / (pixel[1] - camera[1])

        floor_contact = [
            camera[0] + (t * (pixel[0] - camera[0])),
            camera[1] + (t * (pixel[1] - camera[1])),
            camera[2] + (t * (pixel[2] - camera[2])),
        ]

        return True


def colorFloor(
    pixel,
):
    turtle.penup()
    turtle.pencolor(0.4, 0.4, 0.4)
    turtle.goto(pixel[0], pixel[1])
    turtle.dot()
    turtle.pendown()
    turtle.penup()


for x in range(-200, 200, 1):
    for y in range(-200, 200, 1):
        pixel[0] = x
        pixel[1] = y

        if checkIntersections_sphere(camera, pixel, sphere, radius):
            getColor(sphere, point_of_contact)
            color(pixel, diffrence_in_angle, spector)
        else:
            if checkFloor(camera, pixel, floorLevel):

                if checkIntersections_sphere(floor_contact, light, sphere, radius):

                    colorFloor(pixel)
sphere = [200, 0, 101]
camera = [200, 0, -100]

for x in range(50, 475, 1):
    for y in range(-200, 200, 1):
        pixel[0] = x
        pixel[1] = y

        if checkIntersections_sphere(camera, pixel, sphere, radius):
            getColor(sphere, point_of_contact)
            color(pixel, diffrence_in_angle, spector)
        else:
            if checkFloor(camera, pixel, floorLevel):

                if checkIntersections_sphere(floor_contact, light, sphere, radius):

                    colorFloor(pixel)
sphere = [-200, 0, 101]
camera = [-200, 0, -100]

for x in range(-800, 75, 1):
    for y in range(-200, 200, 1):
        pixel[0] = x
        pixel[1] = y

        if checkIntersections_sphere(camera, pixel, sphere, radius):
            getColor(sphere, point_of_contact)
            color(pixel, diffrence_in_angle, spector)
        else:
            if checkFloor(camera, pixel, floorLevel):

                if checkIntersections_sphere(floor_contact, light, sphere, radius):

                    colorFloor(pixel)
window.mainloop()
\$\endgroup\$
4
  • \$\begingroup\$ What are you more confused about in particular? Consider formatting your code with Black and avoiding global. \$\endgroup\$
    – ggorlen
    Mar 9 at 3:23
  • \$\begingroup\$ Im confused about how people use the graphics cards, and create different and multiple shapes in a scene. This project is the edge of my knowledge. \$\endgroup\$ Mar 9 at 4:33
  • \$\begingroup\$ Thanks for that (and for the code formatting), but this is still hard to respond to. Without detail of what's confusing you exactly, it's not easy to offer much clarity (still plenty to offer for a general review, though). \$\endgroup\$
    – ggorlen
    Mar 9 at 4:38
  • \$\begingroup\$ Maybe not being exactly sure of what Im confused about is my problem lol. If you have any general advice Id be glad to take it. \$\endgroup\$ Mar 9 at 4:44

1 Answer 1

5
\$\begingroup\$

Overview

You've done an excellent job:

  • Overall code layout is good
  • You did a good job partitioning code into functions
  • You leveraged code written by others with the imports
  • Used meaningful names for functions and variables

Here are some adjustments for you to consider, mainly for coding style.

Documentation

Add comments at the top of the file to state the purpose of the code. For example:

'''
Draw 3 spheres.
Note: please be patient as it takes about 40 seconds to draw
the high-resolution graphics.
'''

Layout

I recommend moving the functions to the top, after the import statements. Having them in the middle of the code interrupts the natural flow of the code (from a human readability standpoint).

You sometimes have blank lines where there is no need for them and other places where they would be useful. Consider this code:

for x in range(-200, 200, 1):
    for y in range(-200, 200, 1):
        pixel[0] = x
        pixel[1] = y

        if checkIntersections_sphere(camera, pixel, sphere, radius):
            getColor(sphere, point_of_contact)
            color(pixel, diffrence_in_angle, spector)
        else:
            if checkFloor(camera, pixel, floorLevel):

                if checkIntersections_sphere(floor_contact, light, sphere, radius):

                    colorFloor(pixel)
sphere = [200, 0, 101]
camera = [200, 0, -100]

In these nested for loops, I don't think you need any blank lines, but you should have one after the loops:

for x in range(-200, 200, 1):
    for y in range(-200, 200, 1):
        pixel[0] = x
        pixel[1] = y
        if checkIntersections_sphere(camera, pixel, sphere, radius):
            getColor(sphere, point_of_contact)
            color(pixel, diffrence_in_angle, spector)
        else:
            if checkFloor(camera, pixel, floorLevel):
                if checkIntersections_sphere(floor_contact, light, sphere, radius):
                    colorFloor(pixel)

sphere = [200, 0, 101]
camera = [200, 0, -100]

DRY

Since most of the code in those nested loops is repeated 3 times (once for each sphere), consider creating another function for that.

Another place where you repeat code is:

normal_vector = [
    point_of_contact[0] - sphere[0],
    point_of_contact[1] - sphere[1],
    point_of_contact[2] - sphere[2],
]

You can use zip in a list comprehension:

normal_vector = [p - s for p, s in zip(point_of_contact, sphere)]

Naming

According to the PEP 8 style guide, function and variable names should be lowercase, with words separated by underscores as necessary to improve readability.

For example, function colorFloor would be color_floor

The checkIntersections_sphere function returns a boolean value. It is customary to name such functions with the is_ prefix. For example, something like is_point_of_contact might be appropriate, depending on what the function does.

Similarly, checkFloor could be is_floor.

The name of color function could convey a little more meaning. If the function adds a color, it could be named add_color.

Simplicity

In the following line:

turtle.pencolor(1 - diffrence_in_angle, 0 + spector, 0 + spector)

if it is not required to add 0, then the code would be simpler without it:

turtle.pencolor(1 - diffrence_in_angle, spector, spector)

Similarly:

b = (-1 * 2) * (

could be:

b = -2 * (

Lint check

pylint identified a few issues.

It recommends adding docstrings to the functions to summarize what they do.

It also recommends minimizing the usage of global variables.

Performance

To address your concern about the code taking longer than you would like, I recommend you focus on the nested for loops and the functions that perform a lot of calculations.

The for loops use a step size of 1. Increasing the step size speeds things up, but you lose resolution.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.