Perlin Noise in Ursina - python

Is there a way to incorporate Perlin Noise into the Ursina game engine in Python to give it a Minecraft feeling? I think I may be onto something, but for some reason, the y value is not varying. Do you mind helping me out?
The code that I have so far:
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
from perlin_noise import PerlinNoise;
app = Ursina()
noise = PerlinNoise(octaves=10, seed=1)
# Define a Voxel class.
# By setting the parent to scene and the model to 'cube' it becomes a 3d button.
class Voxel(Button):
def __init__(self, position=(0,0,0)):
super().__init__(
parent = scene,
position = position,
model = 'cube',
origin_y = .5,
texture = 'white_cube',
color = color.color(0, 0, random.uniform(.9, 1.0)),
highlight_color = color.lime,
)
def input(self, key):
if self.hovered:
if key == 'left mouse down':
voxel = Voxel(position=self.position + mouse.normal)
if key == 'right mouse down':
destroy(self)
for z in range(8):
for x in range(8):
y = .25 + noise([x, z])
voxel = Voxel(position=(x, y,z))
player = FirstPersonController()
app.run()

As shown in the usage examples, you'll have to scale the noise value with an additional division:
for z in range(8):
for x in range(8):
y = .25 + noise([x/8, z/8])
voxel = Voxel(position=(x, y,z))
You'll probably want to replace that magic number with a constant.

You can use this:
seed=random.randint(1,1000000)
noise = PerlinNoise(octaves=3,seed=seed)
for z in range(8):
for x in range(8):
y = noise([x * 0.02,z * 0.02])
y = math.floor(y * 7.5)
voxel = Voxel(position=(x,y,z))

First, you did not say "Perlin Noise" correctly, so remove the semi-colon, then you'll use this code:
noise = PerlinNoise(octaves=intgeer,seed=0)
for z in range(8):
for x in range(8):
y = 0. 25 noise([x * 0.02,z * 0.02])
voxel = Voxel(position=(x,y,z))

It sounds like Voxel(...) places a voxel at the specified position, right? If so, then you would need to loop over the whole column to place the blocks into.
And if your noise library doesn't handle frequency and amplitude scaling internally, then you need to do that externally. Multiply the input coordinates by fractions, and multiply the output value by larger values.
min_height = 4.0
max_height = 16.0
for z in range(8):
for x in range(8):
height = # some noise formula
# if noise() range is 0 to 1: min_height + noise([x * 0.05, z * 0.05]) * (max_height - min_height)
# if noise() range is -1 to 1: min_height + (noise([x * 0.05, z * 0.05]) * 0.5 + 0.5) * (max_height - min_height)
for y in range(height): # might need to convert height to an integer
voxel = Voxel(position=(x,y,z))
Also please keep in mind the problems of Perlin for noise in its unmitigated form. The algorithm's popularity by name far outweighs its canonical form's true merit in light of its square alignment issues and the broader space of options we have now, and in spite of the overwhelming amount of content that still teaches it in a vacuum. See the final paragraph from my last answer for more info: https://stackoverflow.com/a/73348943
I would suggest this: https://pypi.org/project/pyfastnoiselite/
from pyfastnoiselite.pyfastnoiselite import (
FastNoiseLite, NoiseType, FractalType,
)
min_height = 4.0
max_height = 16.0
noise = FastNoiseLite(0)
noise.noise_type = NoiseType.NoiseType_OpenSimplex2
noise.fractal_type = FractalType.FractalType_FBm
noise.fractal_octaves = 4
noise.frequency = 0.03
for z in range(8):
for x in range(8):
height = min_height + (noise.get_noise(x, z) * 0.5 + 0.5) * (max_height - min_height)
for y in range(height): # might need to convert height to an integer
Voxel(position=(x,y,z))
or for 3D noise to enable overhangs, you vary that heightmap within Y as well:
from pyfastnoiselite.pyfastnoiselite import (
FastNoiseLite, NoiseType, FractalType,
)
min_height = 4.0
max_height = 16.0
noise = FastNoiseLite(0)
noise.noise_type = NoiseType.NoiseType_OpenSimplex2
noise.fractal_type = FractalType.FractalType_FBm
noise.fractal_octaves = 4
noise.frequency = 0.03
for z in range(8):
for x in range(8):
for y in range(min_height): # might need to convert bounds to integers
Voxel(position=(x,y,z))
for y in range(min_height, max_height): # might need to convert bounds to integers
effective_height = min_height + (noise.get_noise(x, y, z) * 0.5 + 0.5) * (max_height - min_height)
if effective_height > y:
Voxel(position=(x,y,z))
Examples untested. There may be some changes to parameters or even syntax to get these working! Feel free to make the needed edits to this answer to get it working properly.
Also min_height + (noise.get_noise(x, y, z) * 0.5 + 0.5) * (max_height - min_height) can be simplified down to noise.get_noise(x, y, z) * factor + offset, where
factor = 0.5 * (max_height - min_height)
offset = factor + min_height

Related

projecting points onto a parallel grid at a depth from camera (maya)

I am attempting to create a grid of locators that serve as projected points onto a parallel finite plane from a camera in maya at a specified depth. The grid should line up with a specified resolution so as to match rendered output.
At the moment my calculations are off and I am looking for some help to ascertain how my formula for ascertaining the projected points is incorrect.
I have a self contained python script and image showing the current position of locators that are spawned as an example.
image showing current spawned locators are off on y and z axis
import maya.cmds as mc
import maya.OpenMaya as om
res = [mc.getAttr('defaultResolution.width'),
mc.getAttr('defaultResolution.height')]
print res
grid = [5, 5]
def projectedGridPoint(camera, coord, depth, res):
selList = om.MSelectionList()
selList.add(camera)
dagPath = om.MDagPath()
selList.getDagPath(0,dagPath)
dagPath.extendToShape()
camMtx = dagPath.inclusiveMatrix()
fnCam = om.MFnCamera(dagPath)
mFloatMtx = fnCam.projectionMatrix()
projMtx = om.MMatrix(mFloatMtx.matrix)
#center of camera
eyePt = fnCam.eyePoint()
#offset position
z = eyePt.z - depth
#calculated xy positions
x = (2 * z * coord[0] / res[0]) - z
y = (2 * z * coord[1] / res[1]) - z
return om.MPoint(x,y,depth) * camMtx * projMtx.inverse()
for y in range(grid[1] + 1):
for x in range(grid[0] + 1):
coord = ( x / float(grid[0]) * res[0], y / float(grid[1]) * res[1] )
pt = projectedGridPoint('camera1', coord, 10, res)
mc.spaceLocator(a=1, p=[pt.x, pt.y, pt.z])
Once I adjusted Theodox's answer to account for all possible grid divisions, such that the ndc_x and ndc_y was always in the range of -1 and 1. I was able to get a working solution.
import maya.api.OpenMaya as om
import maya.cmds as cmds
def projectedGridPoint(camera, coord, depth):
selList = om.MGlobal.getSelectionListByName(camera)
dagPath = selList.getDagPath(0)
dagPath.extendToShape()
view = dagPath.inclusiveMatrix()
fnCam = om.MFnCamera(dagPath)
projection = om.MMatrix(fnCam.projectionMatrix())
viewProj = projection * view
r = om.MPoint(coord[0],coord[1], -1 * depth) * projection.inverse()
return r.homogenize() * view
xx, yy = (6, 6)
for y in range(yy + 1):
for x in range(xx + 1):
ndc_x = -1
ndc_y = -1
if x > 0:
ndc_x = (x / float(xx) * 2) - 1
if y > 0:
ndc_y = (y / float(yy) * 2) - 1
coord = ( ndc_x, ndc_y)
print coord
pt = projectedGridPoint('camera1', coord, 0)
c,_ = cmds.polyCube(w = 0.1, d = 0.1, h = 0.1)
cmds.xform(c, t = (pt[0], pt[1], pt[2]))
I think you want something a bit more like this (note, i converted it to API 2 to cut down on the boilerplate)
import maya.api.OpenMaya as om
import maya.cmds as cmds
def projectedGridPoint(camera, coord, depth):
selList = om.MGlobal.getSelectionListByName(camera)
dagPath = selList.getDagPath(0)
dagPath.extendToShape()
view = dagPath.inclusiveMatrix()
fnCam = om.MFnCamera(dagPath)
projection = om.MMatrix(fnCam.projectionMatrix())
viewProj = projection * view
r = om.MPoint(coord[0],coord[1], -1 * depth) * projection.inverse()
return r.homogenize() * view
xx, yy = (2, 2)
for y in range(yy):
for x in range(xx):
ndc_x = 2.0 * x / float(xx - 1) - 1
ndc_y = 2.0 * y / float(yy - 1) - 1
coord = ( ndc_x, ndc_y)
pt = projectedGridPoint('camera1', coord,0)
c,_ = cmds.polyCube(w = 0.1, d = 0.1, h = 0.1)
cmds.xform(c, t = (pt[0], pt[1], pt[2]))
The coords are supplied as normalized device coordinates (from -1,-1 to 1, 1 at the corners of the view) and the depth goes from the near to far clip planes -- a depth of 1 is right on the near plane and a depth of 0 is on the far plane. I think in practice I'd lock the depth at 0 and use the clip plane setting on the camera to set the depth
edit I rationalized the original, wonky method of converting index values to NDC coordinates

Python random seed for Perlin noise

I've been recently looking to make procedurally generated terrain for games. I saw that Perlin noise was useful for this, and so I gave it a shot. So far, the terrain is generated beautifully. However, whenever I run the program multiple times, the terrain is the exact same. Is there any way to randomize the Perlin noise that's generated?
Code:
from opensimplex import OpenSimplex
import random
from time import time
height = 40
width = height
scale = height / 10
value = [[0 for x in range(width)] for y in range(height)]
gen = OpenSimplex()
def noise(nx, ny):
# Rescale from -1.0:+1.0 to 0.0:1.0
return gen.noise2d(nx, ny) / 2.0 + 0.5
def printBiome(y, x):
if value[y][x] <= 2:
print('O', end = " ")
elif value[y][x] >= 8:
print('M', end = " ")
else:
print('L', end = " ")
for y in range(height):
for x in range(width):
nx = x/width - 0.5
ny = y/height - 0.5
value[y][x] = 10 * noise(1 * scale * nx, 1 * scale * ny) + 0.5 * noise(2 * scale * nx, 2 * scale* ny) + 0.25 * noise(4 * scale * nx, 4 * scale * ny)
for y in range(height):
for x in range(width):
printBiome(y, x)
print()
The OpenSimplex class defaults to using seed=0. To generate a different terrain, input a different seed value:
import uuid
# http://stackoverflow.com/a/3530326/190597
seed = uuid.uuid1().int>>64
gen = OpenSimplex(seed=seed)

How to render Mandelbrot Set faster?

I'm currently drawing the Mandelbrot set pixel by pixel with PhotoImage and tkinter. I'm using basically the algorithm directly with no modifications. Are there methods to make the calculation faster? Maybe filling in large areas of color quickly, or precalcuating constants?
Part of the code:
ITERATIONS = 50
WIDTH, HEIGHT = 600, 600
CENTER = (-.5, 0)
DIAMETER = 2.5
def mandel(c):
z = 0
for i in range(ITERATIONS):
z = z**2 + c
if abs(z) > 2:
return i
return ITERATIONS
root = Tk()
canvas = Canvas(root, width=WIDTH,height=HEIGHT)
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image((WIDTH/2, HEIGHT/2), image=img, state="normal")
real = CENTER[0] - 0.5 * DIAMETER
imag = CENTER[1] - 0.5 * DIAMETER
def color(i):
colors = ("#0000AA", "#88DDFF", "#FF8800", "#000000")
if i == ITERATIONS:
return colors[-1]
else:
choice = (i//2) % len(colors)
return colors[choice]
for x in range(WIDTH):
for y in range(HEIGHT):
i = mandel(complex(real, imag))
img.put(color(i), (x, HEIGHT-y))
imag += DIAMETER / HEIGHT
imag = CENTER[1] - 0.5 * DIAMETER
real += DIAMETER / WIDTH
mainloop()
Setting one pixel at a time is likely the main source of the slowdown. Instead of calling put for each pixel, computer a whole row of pixels, or an entire matrix of pixels, and then call put one time at the end of the loop.
You can find an example here, among other places: https://web.archive.org/web/20170512214049/http://tkinter.unpythonic.net:80/wiki/PhotoImage#Fill_Many_Pixels_at_Once
Here is my code, it draws a 640x480 Mandelbrot in 8-9 seconds.
It does up to 256 iterations per pixel, uses a color map list, 'puts' only once to PhotoImage and doesn't rely on symetry, so it could show any zoomed area of the set.
It's a pity that Tkinter doesn't allow access to the raster information of PhotoImage as a buffer and that the clumsy string is required.
from tkinter import Tk, Canvas, PhotoImage,NW,mainloop
from time import clock
def mandel(kx,ky):
""" calculates the pixel color of the point of mandelbrot plane
passed in the arguments """
global clr
maxIt = 256
c = complex(kx, ky)
z = complex(0.0, 0.0)
for i in range(maxIt):
z = z * z + c
if abs(z) >= 2.0:
return (255-clr[i],0,0)
return(0,0,0)
def prepare_mdb(xa,xb,ya,yb):
""" pre-calculates coordinates of the mandelbrot plane required for each
pixel in the screen"""
global x,y,xm,ym
xm.clear
ym.clear
xm=[xa + (xb - xa) * kx /x for kx in range(x)]
ym=[ya + (yb - ya) * ky /y for ky in range(y)]
x=640
y=480
#corners of the mandelbrot plan to display
xa = -2.0; xb = 1.0
ya = -1.5; yb = 1.5
#precalculated color table
clr=[ int(255*((i/255)**12)) for i in range(255,-1,-1)]
xm=[]
ym=[]
prepare_mdb(xa,xb,ya,yb)
#Tk
window = Tk()
canvas = Canvas(window, width = x, height = y, bg = "#000000")
t1=clock()
img = PhotoImage(width = x, height = y)
canvas.create_image((0, 0), image = img, state = "normal", anchor = NW)
pixels=" ".join(("{"+" ".join(('#%02x%02x%02x' % mandel(i,j) for i in xm))+"}" for j in ym))
img.put(pixels)
canvas.pack()
print(clock()-t1)
mainloop()
Pure python is not that fast for numeric code. The easiest way to speed things up would be to use PyPy. If that is not fast enough, vectorize your algorithms using numpy. If that is still not fast enough, use Cython, or consider rewriting it in C.
For a modest increase in speed (but not enough to offset the difference between a compiled language and an interpreted one), you can precalculate some of the values.
Right now, you're calculating DIAMETER / HEIGHT once per inner loop, and CENTER[1] - 0.5 * DIAMETER as well as DIAMETER / WIDTH once per outer loop. Do this beforehand.
len(colors) also won't change and can be replaced by a constant. In fact, I'd probably write that function as
def color(i):
if i == ITERATIONS:
return "#000000"
else:
return ("#0000AA", "#88DDFF", "#FF8800", "#000000")[(i//2) % 4]
# are you sure you don't want ("#0000AA", "#88DDFF", "#FF8800")[(i//2) % 3] ?
Also, x**2 is slower than x*x (because the x**y operator doesn't shortcut for the trivial case of y==2), so you can speed that calculation up a bit.
Most time is spent in the inner loop in mandel(). z*z instead of z**2 had a slight effect. There is not much else to speed up there that I can see. Removing constants from other loops had little effect, though I tend to prefer doing so. Choosing ITERATIONS so that ITERATIONS//2 % len(colors) == len(colors)-1, as in 46 //2 % 4 == 3, allows simplification of the code. Exploiting symmetry around the x-axis cuts time in half. Starting imag at 0 avoids the roundoff error of 300 subtractions from +/- DIAMETER / 2 and results in a clean center line in the image.
from tkinter import *
ITERATIONS = 46
WIDTH, HEIGHT = 601, 601 # odd for centering and exploiting symmetry
DIAMETER = 2.5
start = (-.5 - DIAMETER / 2, 0) # Start y on centerline
d_over_h = DIAMETER / HEIGHT
d_over_w = DIAMETER / WIDTH
def mandel(c):
z = 0
for i in range(ITERATIONS):
z = z*z + c
if abs(z) > 2:
return i
return ITERATIONS
root = Tk()
canvas = Canvas(root, width=WIDTH,height=HEIGHT)
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image(((WIDTH+1)//2, (HEIGHT+1)//2), image=img, state="normal")
real, imag = start
colors = ("#0000AA", "#88DDFF", "#FF8800", "#000000")
ncolors = len(colors)
yrange = range(HEIGHT//2, -1, -1) # up from centerline
ymax = HEIGHT - 1
for x in range(WIDTH):
for y in yrange:
i = mandel(complex(real, imag))
color = colors[i//2 % ncolors]
img.put(color, (x, y))
img.put(color, (x, ymax - y))
imag += d_over_h
imag = start[1]
real += d_over_w
mainloop()
Complex numbers in python can be slow, especially if you call abs(x) every iteration. Representing the complex number with c_r and c_i, for real and imaginary parts, reduces the number of calculations you do every iteration.
def mandel(c):
z = 0
for i in range(ITERATIONS):
z = z**2 + c
if abs(z) > 2:
return i
return ITERATIONS
instead of z = 0, replace it with z_r,z_i=0,0
we also have to change c in the parameters.
Now we have:
def mandel(c_r,c_i):
z_r = 0
z_i = 0
for i in range(ITERATIONS):
z = z**2 + c
if abs(z) > 2:
return i
return ITERATIONS
Instead of using abs(z) > 2, we can now use z_r * z_r + z_i + z_i > 4
Also, we replace z**2 + c with a new version using our new variables
(Know that (a+bi)^2 = a^2 - b^2 + 2abi
def mandel(c_r,c_i):
z_r = 0
z_i = 0
z_r_squared = 0
z_i_squared = 0
for i in range(ITERATIONS):
z_r_squared = z_r * z_r
z_i_squared = z_i * z_i
z_r = z_r_squared - z_i_squared + c_r
z_i = 2 * z_r * z_i + c_i
if z_r_squared + z_r_squared > 4:
return i
return ITERATIONS
Finally, you have to change where the mandelbrot function is called, so
i = mandel(complex(real, imag))
becomes
i = mandel(real, imag)

Get circle to orbit with given equations using Python graphics.py module

I was given this equation in order to get the circle to orbit. I created an infinite loop assuming it should orbit forever. x = cx + r*cos(t) and y = cy + r*sin(t)
Am I doing something wrong?
from graphics import *
import math
def main():
win=GraphWin("Cirlce",600,600)
x=250
y=70
c=Circle(Point(x,y),18)
c.draw(win)
v=True
while v==True:
c.undraw()
x = x + c.getRadius()*math.cos(2)
y = y + c.getRadius()*math.sin(2)
c=Circle(Point(x,y),18)
c.draw(win)
main()
The problem is here:
x = x + c.getRadius()*math.cos(2)
y = y + c.getRadius()*math.sin(2)
You are moving in a straight line. And, as your code runs quite fast, it probably goes out of scope quite quickly. The correct version would be:
x0, y0 = 0, 0 # Coordinates of the centre
r = 2 # Radius
t = 0
dt = 0.01 # Or anything that looks smooth enough.
while True: # No need for an extra variable here
c.undraw() # I don't know this graphics library
# I will assume what you did is correct
x = x0 + r * math.cos(t)
y = y0 + r * math.sin(t)
c=Circle(Point(x,y),18)
c.draw(win)
t += dt
time.sleep(0.01)
At the end of the loop I send it to sleep for a bit so it goes at a finite pace. Some graphics libraries include a rate function, that allows you to run it at a fixed number of frames per second, independently of how fast the loop is in your machine.
c.undraw() # I don't know this graphics library, I will assume what
you did is correct
Using c.undraw(), c = Circle(...), and c.draw() on every loop iteration seems wasteful when GraphicsObjects can move(dx, dy). However, the tricky part is the movement is relative so you have to calculate the difference to your next position:
import math
import time
from graphics import *
WINDOW_WIDTH, WINDOW_HEIGHT = 600, 600
win = GraphWin("Circle", WINDOW_WIDTH, WINDOW_HEIGHT)
win.setCoords(-WINDOW_WIDTH / 2, -WINDOW_HEIGHT / 2, WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2)
ORBIT_RADIUS = 200
PLANET_RADIUS = 18
SOLAR_RADIUS = 48
x0, y0 = 0, 0 # Coordinates of the center
t = 0.0
dt = 0.01 # Or anything that looks smooth enough.
delay = 0.01
star = Circle(Point(x0, y0), SOLAR_RADIUS)
star.setFill("yellow")
star.draw(win)
orbit = Circle(Point(x0, y0), ORBIT_RADIUS)
orbit.setOutline("lightgray")
orbit.draw(win)
planet = Circle(Point(x0 + ORBIT_RADIUS * math.cos(t), y0 + ORBIT_RADIUS * math.sin(t)), PLANET_RADIUS)
planet.setFill("blue")
planet.draw(win)
while True:
x, y = x0 + ORBIT_RADIUS * math.cos(t), y0 + ORBIT_RADIUS * math.sin(t)
center = planet.getCenter()
planet.move(x - center.getX(), y - center.getY())
t = (t + dt) % (2 * math.pi)
if win.checkMouse() is not None:
break
time.sleep(delay)
win.close()
I added a star as it's hard to appreciate that something is orbiting without seeing what's being orbited:
You can click on the window to exit cleanly.

Drawing directions fields

Is there a way to draw direction fields in python?
My attempt is to modify http://www.compdigitec.com/labs/files/slopefields.py giving
#!/usr/bin/python
import math
from subprocess import CalledProcessError, call, check_call
def dy_dx(x, y):
try:
# declare your dy/dx here:
return x**2-x-2
except ZeroDivisionError:
return 1000.0
# Adjust window parameters
XMIN = -5.0
XMAX = 5.0
YMIN = -10.0
YMAX = 10.0
XSCL = 0.5
YSCL = 0.5
DISTANCE = 0.1
def main():
fileobj = open("data.txt", "w")
for x1 in xrange(int(XMIN / XSCL), int(XMAX / XSCL)):
for y1 in xrange(int(YMIN / YSCL), int(YMAX / YSCL)):
x= float(x1 * XSCL)
y= float(y1 * YSCL)
slope = dy_dx(x,y)
dx = math.sqrt( DISTANCE/( 1+math.pow(slope,2) ) )
dy = slope*dx
fileobj.write(str(x) + " " + str(y) + " " + str(dx) + " " + str(dy) + "\n")
fileobj.close()
try:
check_call(["gnuplot","-e","set terminal png size 800,600 enhanced font \"Arial,12\"; set xrange [" + str(XMIN) + ":" + str(XMAX) + "]; set yrange [" + str(YMIN) + ":" + str(YMAX) + "]; set output 'output.png'; plot 'data.txt' using 1:2:3:4 with vectors"])
except (CalledProcessError, OSError):
print "Error: gnuplot not found on system!"
exit(1)
print "Saved image to output.png"
call(["xdg-open","output.png"])
return 0
if __name__ == '__main__':
main()
However the best image I get from this is.
How can I get an output that looks more like the first image? Also, how can I add the three solid lines?
You can use this matplotlib code as a base. Modify it for your needs.
I have updated the code to show same length arrows. The important option is to set the angles option of the quiver function, so that the arrows are correctly printed from (x,y) to (x+u,y+v) (instead of the default, which just takes into account of (u,v) when computing the angles).
It is also possible to change the axis form "boxes" to "arrows". Let me know if you need that change and I could add it.
import matplotlib.pyplot as plt
from scipy.integrate import odeint
import numpy as np
fig = plt.figure()
def vf(x, t):
dx = np.zeros(2)
dx[0] = 1.0
dx[1] = x[0] ** 2 - x[0] - 2.0
return dx
# Solution curves
t0 = 0.0
tEnd = 10.0
# Vector field
X, Y = np.meshgrid(np.linspace(-5, 5, 20), np.linspace(-10, 10, 20))
U = 1.0
V = X ** 2 - X - 2
# Normalize arrows
N = np.sqrt(U ** 2 + V ** 2)
U = U / N
V = V / N
plt.quiver(X, Y, U, V, angles="xy")
t = np.linspace(t0, tEnd, 100)
for y0 in np.linspace(-5.0, 0.0, 10):
y_initial = [y0, -10.0]
y = odeint(vf, y_initial, t)
plt.plot(y[:, 0], y[:, 1], "-")
plt.xlim([-5, 5])
plt.ylim([-10, 10])
plt.xlabel(r"$x$")
plt.ylabel(r"$y$")
I had a lot of fun making one of these as a hobby project using pygame. I plotted the slope at each pixel, using shades of blue for positive and shades of red for negative. Black is for undefined. This is dy/dx = log(sin(x/y)+cos(y/x)):
You can zoom in & out - here is zoomed in on the middle upper part here:
and also click on a point to graph the line going through that point:
It's just 440 lines of code, so here is the .zip of all the files. I guess I'll excerpt relevant bits here.
The equation itself is input as a valid Python expression in a string, e.g. "log(sin(x/y)+cos(y/x))". This is then compiled. This function here graphs the color field, where self.func.eval() gives the dy/dx at the given point. The code is a bit complicated here because I made it render in stages - first 32x32 blocks, then 16x16, etc. - to make it snappier for the user.
def graphcolorfield(self, sqsizes=[32,16,8,4,2,1]):
su = ScreenUpdater(50)
lastskip = self.xscreensize
quitit = False
for squaresize in sqsizes:
xsquaresize = squaresize
ysquaresize = squaresize
if squaresize == 1:
self.screen.lock()
y = 0
while y <= self.yscreensize:
x = 0
skiprow = y%lastskip == 0
while x <= self.xscreensize:
if skiprow and x%lastskip==0:
x += squaresize
continue
color = (255,255,255)
try:
m = self.func.eval(*self.ct.untranscoord(x, y))
if m >= 0:
if m < 1:
c = 255 * m
color = (0, 0, c)
else:
#c = 255 - 255 * (1.0/m)
#color = (c, c, 255)
c = 255 - 255 * (1.0/m)
color = (c/2.0, c/2.0, 255)
else:
pm = -m
if pm < 1:
c = 255 * pm
color = (c, 0, 0)
else:
c = 255 - 255 * (1.0/pm)
color = (255, c/2.0, c/2.0)
except:
color = (0, 0, 0)
if squaresize > 1:
self.screen.fill(color, (x, y, squaresize, squaresize))
else:
self.screen.set_at((x, y), color)
if su.update():
quitit = True
break
x += xsquaresize
if quitit:
break
y += ysquaresize
if squaresize == 1:
self.screen.unlock()
lastskip = squaresize
if quitit:
break
This is the code which graphs a line through a point:
def _grapheqhelp(self, sx, sy, stepsize, numsteps, color):
x = sx
y = sy
i = 0
pygame.draw.line(self.screen, color, (x, y), (x, y), 2)
while i < numsteps:
lastx = x
lasty = y
try:
m = self.func.eval(x, y)
except:
return
x += stepsize
y = y + m * stepsize
screenx1, screeny1 = self.ct.transcoord(lastx, lasty)
screenx2, screeny2 = self.ct.transcoord(x, y)
#print "(%f, %f)-(%f, %f)" % (screenx1, screeny1, screenx2, screeny2)
try:
pygame.draw.line(self.screen, color,
(screenx1, screeny1),
(screenx2, screeny2), 2)
except:
return
i += 1
stx, sty = self.ct.transcoord(sx, sy)
pygame.draw.circle(self.screen, color, (int(stx), int(sty)), 3, 0)
And it runs backwards & forwards starting from that point:
def graphequation(self, sx, sy, stepsize=.01, color=(255, 255, 127)):
"""Graph the differential equation, given the starting point sx and sy, for length
length using stepsize stepsize."""
numstepsf = (self.xrange[1] - sx) / stepsize
numstepsb = (sx - self.xrange[0]) / stepsize
self._grapheqhelp(sx, sy, stepsize, numstepsf, color)
self._grapheqhelp(sx, sy, -stepsize, numstepsb, color)
I never got around to drawing actual lines because the pixel approach looked too cool.
Try changing your values for the parameters to this:
XSCL = .2
YSCL = .2
These parameters determine how many points are sampled on the axes.
As per your comment, you'll need to also plot the functions for which the derivation dy_dx(x, y) applies.
Currently, you're only calculating and plotting the slope lines as calculated by your function dy_dx(x,y). You'll need to find (in this case 3) functions to plot in addition to the slope.
Start by defining a function:
def f1_x(x):
return x**3-x**2-2x;
and then, in your loop, you'll have to also write the desired values for the functions into the fileobj file.

Categories