I have spent the last couple of weeks in my off-time looking at openGL. And while I do not have a problem following some of the older NeHe examples, from everything I have read, OpenGL4 is a totally different process. And I have access to the red book and the super bible, but the former is still offering legacy opengl calls where as the latter uses their own library. Neither is especially helpful in understanding how to put together code in a project. For example, my current understanding is that glu and glut are legacy and shouldn't be used for opengl 4.
I can generate vertices very easily for a hypothetical model space. I have an extremely hard time understanding how a model ends up showing up on my screen. About 95% of my attempts end up with a black blank screen.
Thanks in advance.
Here's some code:
# primatives.py
from collections import Iterable
from functools import reduce
import operator
import numpy as np
from exc import UnimplementedMethod
class Primative(object):
SIZE = 1 # number of pixels on a default grid
def __init__(self, point=None, *args, **kwargs):
self.point = point if isinstance(point, Iterable) else [0, 0, 0]
self.point = np.array(self.point, dtype=np.float32)
scaler = [self.SIZE/2]*len(self.point)
self.point = (self.point * scaler).tolist()
#property
def active(self):
attr = "__active__"
if not hasattr(self, attr):
setattr(self, attr, False)
return getattr(self, attr)
#active.setter
def active(self, value):
attr = "__active__"
if value in [True, False]:
setattr(self, attr, value)
return getattr(self, attr)
#property
def vertices(self):
"""Returns a simple list of calculated vertices"""
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
#property
def dimension(self):
return len(self.point)
#property
def scaler(self):
attr = "__scaler__"
if not hasattr(self, attr):
size = self.SIZE / 2
setattr(self, attr, [size]*self.dimension)
return getattr(self, attr)
#scaler.setter
def scaler(self, *values):
attr = "__scaler__"
values = values[0] if len(values) == 1 else values
if len(values) == 1 and len(values) != self.point:
if isinstance(values, [int, float]):
setattr(self, attr, [values]*self.dimension)
elif isinstance(values, Iterable):
data = [(v, i)
for v, i in zip(values, xrange(self.dimension))]
value = [v for v, i in data]
if len(value) != self.dimension:
raise ValueError
setattr(self, attr, value)
#property
def translation(self):
attr = "__transalation__"
if not hasattr(self, attr):
size = self.SIZE / 2
setattr(self, attr, [size]*self.dimension)
return getattr(self, attr)
#translation.setter
def transalation(self, *values):
attr = "__transalation__"
values = values[0] if len(values) == 1 else values
if isinstance(values, (int, float)):
setattr(self, attr, [values]*self.dimension)
elif isinstance(values, Iterable):
data = [(v, i)
for v, i in zip(values, xrange(self.dimension))]
value = [v for v, i in data]
if len(value) != self.dimension:
raise ValueError
setattr(self, attr, value)
#property
def rotation(self):
"""
Rotation in radians
"""
attr = "__rotation__"
if not hasattr(self, attr):
setattr(self, attr, [0]*self.dimension)
return getattr(self, attr)
#rotation.setter
def rotation(self, *values):
"""
Rotation in radians
"""
attr = "__rotation__"
values = values[0] if len(values) == 1 else values
if isinstance(values, (int, float)):
setattr(self, attr, [values]*self.dimension)
elif isinstance(values, Iterable):
data = [(v, i)
for v, i in zip(values, xrange(self.dimension))]
value = [v for v, i in data]
if len(value) != self.dimension:
raise ValueError
setattr(self, attr, value)
#property
def volume(self):
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
class Cube(Primative):
# G H
# * --------- *
# /| /|
# C / | D / |
# * --------- * |
# | * -------|- *
# | / E | / F
# |/ |/
# * --------- *
# A B
#property
def center_of_mass(self):
"""
Uses density to calculate center of mass
"""
return self.point
#property
def material(self):
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
#material.setter
def material(self, value):
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
#property
def mass(self):
return self.material.density * self.volume
#property
def volume(self):
func = operator.mul
return reduce(func, self.scaler, 1)
#property
def normals(self):
"""
computes the vertex normals
"""
norm = []
if len(self.point) == 1:
norm = [
# counter clockwise
# x (left hand rule)
(-1), # A
(1) # B
]
elif len(self.point) == 2:
norm = [
# counter clockwise
# x, y (left hand rule)
(-1, -1), # A
(1, -1), # B
(1, 1), # C
(-1, 1) # D
]
elif len(self.point) == 3:
norm = [
# counter clockwise
# x, y, z (left hand rule)
(-1, -1, 1), # A 0
(1, -1, 1), # B 1
(1, 1, 1), # D 2
(-1, 1, 1), # C 3
(-1, -1, -1), # E 4
(1, -1, -1), # F 5
(1, 1, -1), # H 6
(-1, 1, -1), # G 7
]
return norm
#property
def indices(self):
indices = []
if len(self.point) == 2:
indices = [
[[1, 0, 3], [2, 3, 1]], # BAC CDB front
]
elif len(self.point) == 3:
indices = [
[[1, 0, 3], [2, 3, 1]], # BAC CDB front
[[5, 1, 2], [2, 6, 5]], # FBD DHF right
[[4, 5, 6], [6, 7, 4]], # EFH HGE back
[[5, 4, 0], [0, 1, 5]], # FEA ABF bottom
[[0, 4, 7], [7, 3, 0]], # AEG GCA left
[[2, 3, 7], [7, 6, 2]], # DCG GHD top
]
return indices
#property
def nodes(self):
normals = np.array(self.normals, dtype=np.float32)
scaler = np.array(self.scaler, dtype=np.float32)
nodes = normals * scaler
return nodes.tolist()
#property
def vertices(self):
verts = (n for node in self.nodes for n in node)
return verts
And one more:
# Voxel.py
from collections import Iterable
from time import time
import numpy as np
import pyglet
from pyglet.gl import *
from primatives import Cube
import materials
class Voxel(Cube):
"""
Standard Voxel
"""
def __init__(self, point=None, material=None):
super(Voxel, self).__init__(point=point)
if isinstance(material, materials.Material):
self.material = material
else:
self.material = materials.stone
def __str__(self):
point = ", ".join(str(p) for p in self.point)
material = self.material.name
desc = "<Voxel [%s] (%s)>" % (material, point)
return desc
def __repr__(self):
point = ", ".join(str(p) for p in self.point)
material = self.material.name
desc = "<Voxel %s(%s)>" % (material, point)
return desc
#property
def material(self):
attr = "__material__"
if not hasattr(self, attr):
setattr(self, attr, materials.ether)
return getattr(self, attr)
#material.setter
def material(self, value):
attr = "__material__"
if value in materials.valid_materials:
setattr(self, attr, value)
return getattr(self, attr)
class Chunk(Cube):
"""
A Chunk contains a specified number of Voxels. Chunks are an
optimization to manage voxels which do not change often.
"""
NUMBER = 16
NUMBER_OF_VOXELS_X = NUMBER
NUMBER_OF_VOXELS_Y = NUMBER
NUMBER_OF_VOXELS_Z = NUMBER
def __init__(self, point=None):
point = (0, 0, 0) if point is None else point
super(Chunk, self).__init__(point=point)
self.batch = pyglet.graphics.Batch()
points = []
x_scale = self.NUMBER_OF_VOXELS_X / 2
y_scale = self.NUMBER_OF_VOXELS_Y / 2
z_scale = self.NUMBER_OF_VOXELS_Z / 2
self.rebuild_mesh = True
if len(point) == 1:
points = ((x,) for x in xrange(-x_scale, x_scale))
elif len(point) == 2:
points = ((x, y)
for x in xrange(-x_scale, x_scale)
for y in xrange(-y_scale, y_scale))
elif len(point) == 3:
points = ((x, y, z)
for x in xrange(-x_scale, x_scale)
for y in xrange(-y_scale, y_scale)
for z in xrange(-z_scale, z_scale))
t = time()
self.voxels = dict((point, Voxel(point)) for point in points)
self.active_voxels = dict((p, v)
for p, v in self.voxels.iteritems()
if v.active)
self.inactive_voxels = dict((p, v)
for p, v in self.voxels.iteritems()
if not v.active)
print 'Setup Time: %s' % (time() - t)
#property
def material(self):
return ether
#material.setter
def material(self, value):
if value in materials.valid_materials:
for voxel in self.voxels:
if voxel.material != value:
voxel.material = value
self.rebuild_mesh = True
#property
def mesh(self):
"""
Returns the verticies as defined by the Chunk's Voxels
"""
attr = "__mesh__"
if self.rebuild_mesh == True:
self.mesh_vert_count = 0
vertices = []
t = time()
for point, voxel in self.active_voxels.iteritems():
if voxel.active is True:
vertices.extend(voxel.vertices)
num_verts_in_voxel = len(voxel.normals)
self.mesh_vert_count += num_verts_in_voxel
print "Mesh Generation Time: %s" % time() - t
vertices = tuple(vertices)
setattr(self, attr, vertices)
voxel_count = len(self.active_voxels)
voxel_mesh = self.mesh
count = self.mesh_vert_count
group = None
data = ('v3f/static', vertices)
self.batch.add(count, self.mode, group, data)
return getattr(self, attr)
#property
def center_of_mass(self):
"""
Uses density to calculate center of mass. This is probably only
useful if the chunk represents an object.
"""
center = self.point
points = []
for point, voxel in self.active_voxels.iteritems():
mass = voxel.mass
if mass > 0:
point = [p*mass for p in point]
points.append(point)
points = np.array(points)
means = []
if points.any():
for idx, val in enumerate(self.point):
means.append(np.mean(points[:, idx]))
if means:
center = means
return center
def add(self, voxel):
added = False
point = None
if isinstance(voxel, Voxel):
point = voxel.point
elif isinstance(voxel, Iterable):
point = voxel
if point in self.inactive_voxels.iterkeys():
last = self.voxels[point]
self.voxels[point] = voxel if isinstance(voxel, Voxel) else last
self.voxels[point].active = True
self.active_voxels[point] = self.voxels[point]
self.inactive_voxels.pop(point)
added = True
self.rebuild_mesh = True
return added
def remove(self, voxel):
removed = False
point = None
if isinstance(voxel, Voxel):
point = voxel.point
elif isinstance(voxel, Iterable):
point = voxel
if point in self.active_voxels.iterkeys():
last = self.voxels[point]
self.voxels[point] = voxel if isinstance(voxel, Voxel) else last
self.voxels[point].active = False
self.inactive_voxels[point] = self.voxels[point]
self.active_voxels.pop(point)
removed = True
self.rebuild_mesh = True
return removed
def render(self):
voxels = len(self.active_voxels)
self.batch.draw()
return voxels
if __name__ == "__main__":
import pyglet
from pyglet.gl import *
class Window(pyglet.window.Window):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
vox_cnt = self.setup_scene()
print 'Added: %s voxels' % (vox_cnt)
def run(self):
"""wrapper to start the gui loop"""
pyglet.app.run()
def setup_scene(self):
self.chunk = Chunk()
cnt = 0
t = time()
for x in xrange(self.chunk.NUMBER_OF_VOXELS_X):
for y in xrange(self.chunk.NUMBER_OF_VOXELS_Y):
self.chunk.add((x, y))
cnt += 1
print "Setup Scene Time: %s" % (time() - t)
return cnt
def render_scene(self):
y = h = self.height
x = w = self.width
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
# glEnable(GL_DEPTH_TEST)
# glDepthFunc(GL_LESS)
t = time()
voxels_drawn = self.chunk.render()
print 'Render Time: %s' % (time() - t)
print 'Points Rendered %s' % voxels_drawn
# array_len = len(self.vertex_data)
# glDrawArrays(GL_TRIANGLES, 0, array_len)
def on_draw(self, *args, **kwargs):
self.render_scene()
w = Window()
w.run()
There are examples in the source distributions, if you download them (link to page).
The one you want to see is in <top-dir>/examples/opengl.py -- for a torus. If you make the following modifications you will have a cube.
# line 91:
cube.draw() # previously torus.draw()
# line 178: replace the line with the below (GL_TRIANGLES for GL_QUADS)
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, indices)
# line 187:
cube = Cube(0.8) # previously torus = Torus(1, 0.3, 50, 30)
# replace lines 125 through 166 with:
class Cube(object):
Vertices =(0.,0.,0., 1.,0.,0., 0.,0.,1., 1.,0.,1.,
0.,1.,0., 1.,1.,0., 0.,1.,1., 1.,1.,1.)
def __init__(self, scale):
# Create the vertex and normal arrays.
indices = [0,1,3,2, 1,5,7,3, 5,4,6,7,
0,2,6,4, 0,4,5,1, 2,3,7,6]
normals = [ 0.0, -1.0, 0.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
-1.0, 0.0, 0.0,
0.0, 0.0, -1.0,
0.0, 0.0, 1.0]
vertices = [scale * v for v in Cube.Vertices]
vertices = (GLfloat * len(vertices))(*vertices)
normals = (GLfloat * len(normals))(*normals)
I don't know about python's class wrappers and I did not do any graphics programming for quite some time. But I know that you should search for qualified answers about real workings and internals in the community of hardware guys who either create the VHDL GPU code or write low level drivers or so. They KNOW for sure how it works and some FAQ explanation should be available already in their community.
Based on that assumption this is what some Googling gave me to start with:
OpenGL 4.4 API Reference Card - page 7 - (available among top level resources on http://www.opengl.org) shows some simple picture (for 5 years old?) with the rendering pipeline split into
Blue blocks indicate various buffers that feed or get fed by the OpenGL pipeline
Green blocks indicate fixed function stages
Yellow blocks indicate programmable stages
Jarrred Walton's - Return of the DirectX vs. OpenGL Debates points to a 130-page slideshow How OpenGL Can Unlock 15x Performance Gains | NVIDIA Blog. Both articles fall into categories AMD,Intel,NVIDIA,Game Developer Converence
I have dome some simple OpenGL using C, C++, Delphi long ago and my recommendation is to get rid of the python mapping at first altogether. Look for suitable class library with good community with some good support only afterwards you know what you are looking for.
The above are IMHO the waters to start fishing in
Related
This is the python code which uses A* algorithm for finding solution for 8 puzzle problems, I got some error messages, how can I fix it?(The error message is under the code)
There are several object-oriented programming concepts for Problems class, Node class that are implemented to express the problem solution search that you need to understand in order to make the Python program complete. The priority queue is to make the nodes to be explored to be sorted according to their f-evaluation function score and return the min one as the first node to be searched next.
There is also a memorize function to memorize the heuristic value of state as a look-up table so that you don’t need to calculate the redundant computing of heuristic estimation value, so you can ignore it at this point if you don’t understand.
The components you need to implement is to make the abstract part of the program realizable for 8 -puzzle with the successor methods attached to a problem class which consists of initial state and goal state. Make sure the program can run correctly to generate the solution sequence that move the empty tile so that the 8-puzzle can move "Up", "Down", "Left", "Right", from initial state to goal state.
import math
infinity = math.inf
from itertools import chain
import numpy as np
import bisect
class memoize:
def __init__(self, f, memo={}):
self.f = f
self.memo = {}
def __call__(self, *args):
if not str(args) in self.memo:
self.memo[str(args)] = self.f(*args)
return self.memo[str(args)]
def coordinate(state):
index_state = {}
index = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]
for i in range(len(state)):
index_state[state[i]] = index[i]
return index_state
def getInvCount(arr):
inv_count = 0
empty_value = -1
for i in range(0, 9):
for j in range(i + 1, 9):
if arr[j] != empty_value and arr[i] != empty_value and arr[i] > arr[j]:
inv_count += 1
return inv_count
def isSolvable(puzzle) :
inv_count = getInvCount([j for sub in puzzle for j in sub])
return (inv_count % 2 == 0)
def linear(state):
return sum([1 if state[i] != goal[i] else 0 for i in range(9)])
#memoize
def manhattan(state):
index_goal = coordinate(goal)
index_state = coordinate(state)
mhd = 0
for i in range(9):
for j in range(2):
mhd = abs(index_goal[i][j] - index_state[i][j]) + mhd
return mhd
#memoize
def sqrt_manhattan(state):
index_goal = coordinate(goal)
index_state = coordinate(state)
mhd = 0
for i in range(9):
for j in range(2):
mhd = (index_goal[i][j] - index_state[i][j])**2 + mhd
return math.sqrt(mhd)
#memoize
def max_heuristic(state):
score1 = manhattan(state)
score2 = linear(state)
return max(score1, score2)
class PriorityQueueElmt:
def __init__(self,val,e):
self.val = val
self.e = e
def __lt__(self,other):
return self.val < other.val
def value(self):
return self.val
def elem(self):
return self.e
class Queue:
def __init__(self):
pass
def extend(self, items):
for item in items: self.append(item)
class PriorityQueue(Queue):
def __init__(self, order=min, f=None):
self.A=[]
self.order=order
self.f=f
def append(self, item):
queueElmt = PriorityQueueElmt(self.f(item),item)
bisect.insort(self.A, queueElmt)
def __len__(self):
return len(self.A)
def pop(self):
if self.order == min:
return self.A.pop(0).elem()
else:
return self.A.pop().elem()
# Heuristics for 8 Puzzle Problem
class Problem:
def __init__(self, initial, goal=None):
self.initial = initial; self.goal = goal
def successor(self, state):
reachable = []
def get_key(val):
for key, value in index_state.items():
if val == value:
return key
return -1
def candidate(state, Position):
state = state.copy()
zero_index = state.index(0)
swap_index = state.index(get_key(Position))
state[zero_index], state[swap_index] = state[swap_index], state[zero_index]
return state
index_state = coordinate(state)
zero_position = index_state[0]
move_pair = {"left":[zero_position[0], zero_position[1] - 1],
"right":[zero_position[0], zero_position[1] + 1],
"up":[zero_position[0] - 1, zero_position[1]],
"down":[zero_position[0] + 1, zero_position[1]]
}
for action, position in move_pair.items():
#print(action, position)
if get_key(position) != -1:
reachable.append((action, candidate(state, position)))
#print(reachable)
return reachable
def goal_test(self, state):
return state == self.goal
def path_cost(self, c, state1, action, state2):
return c + 1
def value(self):
abstract
class Node:
def __init__(self, state, parent=None, action=None, path_cost=0, depth =0):
self.parent = parent
if parent:
self.depth = parent.depth + 1
else:
self.depth = 0
self.path_cost = path_cost
self.state = state
if action:
self.action = action
else: self.action = "init"
def __repr__(self):
return "Node state:\n " + str(np.array(self.state).reshape(3,3)) +"\n -> action: " + self.action + "\n -> depth: " + str(self.depth)
def path(self):
x, result = self, [self]
while x.parent:
result.append(x.parent)
x = x.parent
return result
def expand(self, problem):
for (act,n) in problem.successor(self.state):
if n not in [node.state for node in self.path()]:
yield Node(n, self, act,
problem.path_cost(self.path_cost, self.state, act, n))
def graph_search(problem, fringe):
closed = {}
fringe.append(Node(problem.initial,depth=0))
while fringe:
node = fringe.pop()
if problem.goal_test(node.state):
return node
if str(node.state) not in closed:
closed[str(node.state)] = True
fringe.extend(node.expand(problem))
return None
def best_first_graph_search(problem, f):
return graph_search(problem, PriorityQueue(min, f))
def astar_search(problem, h = None):
h = h or problem.h
def f(n):
return max(getattr(n, 'f', -infinity), n.path_cost + h(n.state))
return best_first_graph_search(problem, f)
def print_path(path, method):
print("*" * 30)
print("\nPath: (%s distance)" % method)
for i in range(len(path)-1, -1, -1):
print("-" * 15)
print(path[i])
goal = [1, 2, 3, 4, 5, 6, 7, 8, 0]
# Solving the puzzle
puzzle = [7, 2, 4, 5, 0, 6, 8, 3, 1]
if(isSolvable(np.array(puzzle).reshape(3,3))): # even true
# checks whether the initialized configuration is solvable or not
print("Solvable!")
problem = Problem(puzzle,goal)
path = astar_search(problem, manhattan).path()
print_path(path, "manhattan")
path = astar_search(problem, linear).path()
print_path(path, "linear")
path = astar_search(problem, sqrt_manhattan).path()
print_path(path, "sqrt_manhattan")
path = astar_search(problem, max_heuristic).path()
print_path(path, "max_heuristic")
else :
print("Not Solvable!") # non-even false
TypeError Traceback (most recent call last)
<ipython-input-124-2a60ddc8c009> in <module>
9 problem = Problem(puzzle,goal)
10
---> 11 path = astar_search(problem, manhattan).path()
12 print_path(path, "manhattan")
13
<ipython-input-123-caa97275712e> in astar_search(problem, h)
18 def f(n):
19 return max(getattr(n, 'f', -infinity), n.path_cost + h(n.state))
---> 20 return best_first_graph_search(problem, f)
21
22 def print_path(path, method):
<ipython-input-123-caa97275712e> in best_first_graph_search(problem, f)
12
13 def best_first_graph_search(problem, f):
---> 14 return graph_search(problem, PriorityQueue(min, f))
15
16 def astar_search(problem, h = None):
<ipython-input-123-caa97275712e> in graph_search(problem, fringe)
8 if str(node.state) not in closed:
9 closed[str(node.state)] = True
---> 10 fringe.extend(node.expand(problem))
11 return None
12
<ipython-input-121-e5a968bd54f0> in extend(self, items)
18
19 def extend(self, items):
---> 20 for item in items: self.append(item)
21
22 class PriorityQueue(Queue):
<ipython-input-122-db21613469b9> in expand(self, problem)
69
70 def expand(self, problem):
---> 71 for (act,n) in problem.successor(self.state):
72 if n not in [node.state for node in self.path()]:
73 yield Node(n, self, act,
TypeError: cannot unpack non-iterable int object
I got some error messages, how can I fix it?
There is one error message, The pieces of codes you get in the error message are the stack trace, which might help you to know how the execution got at the final point where the error occurred. In this case that is not so important. The essence of the error is this:
for (act,n) in problem.successor(self.state)
TypeError: cannot unpack non-iterable int object
So this means that the successor method returned an int instead of a list.
Looking at the code for successor, I notice that it intends to return a list called reachable, but there is a return statement right in the middle of the code, leaving the largest part of that code unexecuted (so-called "dead code"):
return state
This statement makes no sense where it is positioned. It seems to be an indentation problem: that return belongs inside the function just above it, like this:
def candidate(state, Position):
state = state.copy()
zero_index = state.index(0)
swap_index = state.index(get_key(Position))
state[zero_index], state[swap_index] = state[swap_index], state[zero_index]
return state # <-- indentation!
Often for blender scripts have to calculate an encompassing bounding box from a collection of 3D points, for example sake the default blender cube bounding box as input,
coords = np.array(
[[-1. 1. -1.],
[-1. 1. 1.],
[ 1. -1. -1.],
[ 1. -1. 1.],
[ 1. 1. -1.],
[ 1. 1. 1.]]
)
bfl = coords.min(axis=0)
tbr = coords.max(axis=0)
G = np.array((bfl, tbr)).T
bbox_coords = [i for i in itertools.product(*G)]
The bounding box coords for example case will be the cube coords in same order
Looking for some python "iteration magic" using above and ("left", "right"), ("front", "back"),("top", "bottom") , to make a helper class
>>> bbox = BBox(bfl, tbr)
>>> bbox.bottom.front.left
(-1, -1, -1)
>>> bbox.top.front
(0, -1, 1)
>> bbox.bottom
(0, 0, -1)
ie a corner vertex, center of an edge, center of a rectangle. (the average sum of 1, 2, or 4 corners) In blender top is +Z and front is -Y.
Was originally looking at something like populating a nested dictionary with static calculated values
d = {
"front" : {
"co" : (0, -1, 0),
"top" : {
"co" : (0, -1, 1),
"left" : {"co" : (-1, -1, 1)},
}
}
}
Object-like attribute access for nested dictionary
EDIT
To avoid posting an XY Problem, ie posting in question the way I've been approaching this, have added an answer below with where I was at with it. Apologies as I forgot to mention could instead choose north, south, east and west for x and y axis directions, and desire the ability to change.
Feel that looping over 8 corner verts is the way to go re making the "swizzle" dictionary with vertex index as leaf nodes. The vertex indices of "front" face or top bottom right corner don't change.
It's using this as a base for a class that is instanced with the coordinates or bfl, tbr is where no matter what I do I always feel there is a "better" way to go than what I am doing now.
Here are two similar versions. The idea of both is that you always return
a BBox object and only alter a variable x which indicates which dimensions you have specified via left, right, ...
Finally you have a function which uses x to calculate the center of the
remaining corners.
The first approach uses functions so you have to call them bbox.bottom().front().left().c(). The main difference here is that not all the combinations
top
top left
top right
top left front
...
are computed when creating the object, but only when you call them.
import numpy as np
import itertools
class BBox:
"""
("left", "right"), -x, +x
("front", "back"), -y, +y
("bottom", "top"), -z, +z
"""
def __init__(self, bfl, tbr):
self.bfl = bfl
self.tbr = tbr
self.g = np.array((bfl, tbr)).T
self.x = [[0, 1], [0, 1], [0, 1]]
def c(self): # get center coordinates
return np.mean([i for i in itertools.product(*[self.g[i][self.x[i]] for i in range(3)])], axis=0)
def part(self, i, xi):
assert len(self.x[i]) == 2
b2 = BBox(bfl=self.bfl, tbr=self.tbr)
b2.x = self.x.copy()
b2.x[i] = [xi]
return b2
def left(self):
return self.part(i=0, xi=0)
def right(self):
return self.part(i=0, xi=1)
def front(self):
return self.part(i=1, xi=0)
def back(self):
return self.part(i=1, xi=1)
def bottom(self):
return self.part(i=2, xi=0)
def top(self):
return self.part(i=2, xi=1)
bbox = BBox(bfl=[-1, -1, -1], tbr=[1, 1, 1])
>>> bbox.bottom().front().left().c()
(-1, -1, -1)
>>> bbox.top().front().c()
(0, -1, 1)
>>> bbox.bottom().c()
(0, 0, -1)
The second approach uses attributes which are in itself BBox objects.
When you uncomment the print statement in the init function you get an idea of all the recursive calls which are happening during construction.
So while it might be more complicated to see what is going on here, you have more convenience when accessing the attributes.
class BBox:
def __init__(self, bfl, tbr, x=None):
self.bfl = bfl
self.tbr = tbr
self.g = np.array((bfl, tbr)).T
self.x = [[0, 1], [0, 1], [0, 1]] if x is None else x
# print(self.x) # Debugging
self.left = self.part(i=0, xi=0)
self.right = self.part(i=0, xi=1)
self.front = self.part(i=1, xi=0)
self.back = self.part(i=1, xi=1)
self.bottom = self.part(i=2, xi=0)
self.top = self.part(i=2, xi=1)
def c(self): # get center coordinates
return np.mean([i for i in itertools.product(*[self.g[i][self.x[i]]
for i in range(3)])], axis=0)
def part(self, i, xi):
if len(self.x[i]) < 2:
return None
x2 = self.x.copy()
x2[i] = [xi]
return BBox(bfl=self.bfl, tbr=self.tbr, x=x2)
bbox = BBox(bfl=[-1, -1, -1], tbr=[1, 1, 1])
>>> bbox.bottom.front.left.c()
(-1, -1, -1)
You could also add something like this at the end of the constructor, to remove the invalid attributes. (to prevent stuff like bbox.right.left.c()). They were None before but AttributeError might be more appropriate.
def __init__(self, bfl, tbr, x=None):
...
for name in ['left', 'right', 'front', 'back', 'bottom', 'top']:
if getattr(self, name) is None:
delattr(self, name)
And you could add a __repr__()method as well:
def __repr__(self):
return repr(self.get_vertices())
def get_vertices(self):
return [i for i in itertools.product(*[self.g[i][self.x[i]]
for i in range(3)])]
def c(self): # get center coordinates
return np.mean(self.get_vertices(), axis=0)
bbox.left.front
# [(-1, -1, -1), (-1, -1, 1)]
bbox.left.front.c()
# array([-1., -1., 0.])
EDIT
After coming back to this after a while I think it is better to only add the relevant attributes and not add all and than delete half of them afterwards. So the most compact / convenient class I can come up with is:
class BBox:
def __init__(self, bfl, tbr, x=None):
self.bfl, self.tbr = bfl, tbr
self.g = np.array((bfl, tbr)).T
self.x = [[0, 1], [0, 1], [0, 1]] if x is None else x
for j, name in enumerate(['left', 'right', 'front', 'back', 'bottom', 'top']):
temp = self.part(i=j//2, xi=j%2)
if temp is not None:
setattr(self, name, temp)
def c(self): # get center coordinates
return np.mean([x for x in itertools.product(*[self.g[i][self.x[i]]
for i in range(3)])], axis=0)
def part(self, i, xi):
if len(self.x[i]) == 2:
x2, x2[i] = self.x.copy(), [xi]
return BBox(bfl=self.bfl, tbr=self.tbr, x=x2)
Here is another solution using an iterative approach to create a dictionary:
import numpy
import itertools
directions = ['left', 'right', 'front', 'back', 'bottom', 'top']
dims = np.array([ 0, 0, 1, 1, 2, 2]) # xyz
def get_vertices(bfl, tbr, x):
g = np.array((bfl, tbr)).T
return [v for v in itertools.product(*[g[ii][x[ii]] for ii in range(3)])]
bfl = [-1, -1, -1]
tbr = [1, 1, 1]
d = {}
for i in range(6):
x = [[0, 1], [0, 1], [0, 1]]
x[i//2] = [i % 2] # x[dim[i] = min or max
d_i = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))
for j in np.nonzero(dims != dims[i])[0]:
x[j//2] = [j % 2]
d_ij = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))
for k in np.nonzero(np.logical_and(dims != dims[i], dims != dims[j]))[0]:
x[k//2] = [k % 2]
d_ij[directions[k]] = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))
d_i[directions[j]] = d_ij
d[directions[i]] = d_i
d
# {'left': {'c': array([-1., 0., 0.]),
# 'front': {'c': array([-1., -1., 0.]),
# 'bottom': {'c': array([-1., -1., -1.])},
# 'top': {'c': array([-1., -1., 1.])}},
# 'back': {'c': array([-1., 1., 1.]),
# 'bottom': {'c': array([-1., 1., -1.])},
# 'top': {'c': array([-1., 1., 1.])}},
# ....
You can combine this with your linked question to access the keys of the dict via d.key1.key2.
Where I got to with this.
Have added this as an answer in some way to explain my question better
Looping over the 8 verts of the cube matches the 3 names to each valid corner.
The "swizzle" is a permutation of the three axis directions that make up the corners.
Feeding directly into a self nesting dictionary d[i][j][k] = value is a pain free way to create them. (pprint(d) below)
Happy to this point from there it turns ugly with some duck typing getting and getting the element indices from the simple 8 vert truth table.
For no particular reason made the method that returns the generated class a wrapper tho I'm not using it as such.
import numpy as np
import pprint
import operator
from itertools import product, permutations
from functools import reduce
from collections import defaultdict
class NestedDefaultDict(defaultdict):
def __init__(self, *args, **kwargs):
super(NestedDefaultDict, self).__init__(NestedDefaultDict, *args, **kwargs)
def __repr__(self):
return repr(dict(self))
def set_by_path(root, items, value):
reduce(operator.getitem, items[:-1], root)[items[-1]] = value
def create_bbox_swizzle(cls, dirx=("left", "right"), diry=("front", "back"), dirz=("bottom", "top")):
d = NestedDefaultDict()
data = {}
for i, cnr in enumerate(product(*(dirx, diry, dirz))):
vert = {"index": i}
data[frozenset(cnr)] = i
for perm in permutations(cnr, 3):
set_by_path(d, perm, vert)
pprint.pprint(d)
def wire_up(names, d):
class Mbox:
#property
def co(self):
return self.coords[self.vertices].mean(axis=0)
def __init__(self, coords):
self.coords = np.array(coords)
self.vertices = [v for k, v in data.items() if k.issuperset(names)]
pass
def __repr__(self):
if len(names) == 1:
return f"<BBFace {self.vertices}/>"
elif len(names) == 2:
return f"<BBEdge {self.vertices}/>"
elif len(names) == 3:
return f"<BBVert {self.vertices}/>"
return "<BBox/>"
pass
def f(k, v):
def g(self):
return wire_up(names + [k], v)(self.coords)
return property(g)
for k, v in d.items():
if isinstance(v, dict):
setattr(Mbox, k, (f(k, v)))
else:
setattr(Mbox, k, v)
return Mbox
return wire_up([], d)
#create_bbox_swizzle
class BBox:
def __init__(self, *coords, **kwargs):
pass
Test drive:
>>> bbox = BBox(coords) # used coords instead of corners
>>> bbox.co
array([ 5.96046448e-08, -1.19209290e-07, 0.00000000e+00])
>>> bbox.left.bottom
<BBEdge [0, 2]/>
>>> bbox.left.bottom.vertices
[0, 2]
>>> bbox.left.bottom.co
array([-1.00000036e+00, -1.19209290e-07, 0.00000000e+00])
I am trying to make a matrix and tensor in python without importing any modules such as numpy. Would there be a way to do this?
A matrix is just a list of lists. You can do so as such:
x = [[0, 1, 2, 3, 4],
[1, 2, 3, 4, 5],
[3, 4, 5, 6, 7]]
As far as performing operations without numpy goes, that will be up to you to create functions for likely using nested loops.
It would be a list of lists, e.g.:
matrix = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 0],
]
You would then have to implement all of the mathematical operations (matrix multiplication etc) on top of that data structure.
Python reserves the method name __matmul__ for matrix multiplication
class Matrix:
def __mul__(left, right):
print("__mul__ was called")
def __matmul__(left, right):
print("__MATMUL__ WAS CALLED")
def __rmatmul__(right, left):
print(40*"#")
print("__rmatmul__")
print("left == ", left)
print("right == ", right)
print(40 * "#")
def __imatmul__(total, step):
print("__imatmul__")
a = Matrix()
b = Matrix()
a * b # scalar multiplication __mul__
a # b # matrix multiplication __matmul__
3 # b # matrix multiplication __rmatmul__
a #= b
__imatmul__ is similar to the following:
x = 5
x += 2 # __iadd__(x, 2) `x = x + 2`
x *= 1 # __imul__(x, 3) `x = x * 3`
Whenever you write x * y python attempts to get the definition of multiplication from the left-hand argument first. That is, x * y is initially type(x).__add__(x, y)
However, sometimes the left-hand thing doesn't know how to multiply itself by the right-hand thing.
class K:
pass
a = K()
result = 55*a
result = type(55).__mul__(55, a)
The int class does not know how to multiply together 55 and an instance of class K. If type(left).__mul__(left, right) fails, then the back-up mechanism type(right).__rmul__(right, left) is called. If you write your own matrix class, then 3 __rmatmul__ and __rmul__ are what will be called when you put a scalar multiple out in front, like 88.
m = Matrix()
88 * m # __rmul__(m, 88)
88 # m # __rmatmul__(m, 88)
One way to implement a matrix is as a list of lists:
matrix = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 0],
]
This has several notable disadvantages. One is that it is easy to select a row of the matrix:
matrix[1] == [0, 1, 0]
However, selecting columns would be very inefficient:
def get_column(coli:int):
coli = 1
column = list()
for rowi in range(len(matrix)):
column.append(matrix[rowi][coli])
return column
One solution would be to have two different lists of lists:
one in "row-major" order.
the other in "column-major" order.
row_then_col = [
[[("r0", "c0")], [("r0", "c1")], [("r0", "c2")]],
[[("r1", "c0")], [("r1", "c1")], [("r1", "c2")]],
[[("r2", "c0")], [("r2", "c1")], [("r2", "c2")]],
]
ncols = 3
nrows = 3
col_then_row = list()
for coli in range(ncols):
col_then_row.append([None]*ncols)
for rowi in range(nrows):
col_then_row[coli]
col_then_row[coli][rowi] = row_then_col[rowi][coli]
Then col_then_row[coli] will return a whole column and row_then_col[rowi] will return a whole row. You can fake "pointers" with a list of one element. This is will allow a change in col_then_row to automatically be visible in row_then_col, a vis versa, without updating anything.
row_then_col[1][2][0] = "happy"
print(col_then_row[2][1][0]) # "happy"
There are many algorithms for matrix multiplication. I recommend implementing Strassen's algorithm. It is not the fastest in the world, but it is easier to understand than the truly fast ones.
There are many ways to implement matrices. The beginnings of one implementation is shown below:
import io
import math
import abc
def flatten(container):
for elem in container:
if not hasattr(elem, "__iter__"):
yield elem
else:
it_elem = iter(elem)
subelem = next(it_elem)
if subelem != elem:
yield subelem
for j in flatten(it_elem):
yield j
class MatrixNodeState(abc.ABC):
"""
Abstract Base Class
"""
pass
MatrixNodeState.MatrixNodeState = MatrixNodeState
class MatrixNodeStateNullNullNullClass(MatrixNodeState):
#classmethod
def ERR(cls):
with io.StringIO() as string_stream:
print(
"YOU ARE OFF THE EDGE OF THE MAP!",
"STOP ITERATING!",
file=string_stream
)
msg = string_stream.getvalue()
raise cls.OFF_THE_EDGE_OF_THE_MAP(msg)
class OFF_THE_EDGE_OF_THE_MAP(Exception):
pass
def __getattribute__(self, *args):
type(self).ERR()
def __setattr__(self, *args):
type(self).ERR()
MatrixNodeState.nullnullnull = MatrixNodeStateNullNullNullClass()
class MatrixNodeStateNullNullClass(MatrixNodeState):
def __setattr__(*args):
pass
def __getattribute__(self, *args):
return type(self).nullnullnull
MatrixNodeState.nullnull = MatrixNodeStateNullNullClass()
class MatrixNodeStateNullClass(MatrixNodeState):
"""
This class exists because `None.left = n`
would produce an error
`null.left = k` -----`no operation`.
Does nothing
Does not set the `left` attribute
of the nullnull node.
`x = node.left` returns `nullnull`
"""
def __setattr__(*args):
pass
def __getattribute__(self, *args):
return type(self).nullnull
MatrixNodeState.null = MatrixNodeStateNullClass()
class MatrixNodeStateNonNullNull(MatrixNodeState):
def __init__(self, data):
self.data = data
self.up = type(self).null
self.right = type(self).null
self.down = type(self).null
self.left = type(self).null
def __setattr__(self, key, value):
if isinstance(value, type(self).nullnull):
value = type(self).null
elif isinstance(value, type(self).nullnullnull):
value = type(self).null
super().__setattr__(self, key, value)
MatrixNodeState.MatrixNodeStateNonNullNull = MatrixNodeStateNonNullNull
class MatrixNode:
def __init__(self, data=None):
MatrixNodeState = type(self)
if data:
self.state = MatrixNodeState.MatrixNodeStateNonNullNull(data)
else:
self.state = MatrixNodeState.nullnull
def __getattr__(self, attrname):
return self.state.attrname
def __setattr__(self, attr_name, attr_value):
try:
object.__getattr__(self)
super().__setattr__(self, attr_name, attr_value)
except AttributeError:
setattr(self.state, attr_name, attr_value)
MatrixNode.MatrixNodeState = MatrixNodeState
class Matrix:
"""
"""
MatrixNode = MatrixNode
def __init__(self, xdims, xelems):
"""
Example 1:
m = Matrix([3, 3], [1, 0, 0, 0, 1, 0, 0, 0, 1])
Example 2
m = Matrix([3, 3], [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
"""
MatrixNode = type(self).MatrixNode
idims = tuple(map(int, xdims))
ielems = iter(flatten(xelems))
nrows = idims[0]
ncols = idims[1]
self.d = dict()
try:
elem_count = 0
left_node = MatrixNode.nullnull
up_node = MatrixNode.nullnull
for rowi in range(nrows):
for coli in range(ncols):
ielem = next(ielem)
elem_count += 1
up_node = left_node.up.right
node = MatrixNode(ielem)
self.d[(rowi, coli)] = node
node.left = left_node
left_node.right = node
node.up = up_node
up_node.down = node
left_node = node
except StopIteration:
with io.StringIO() as string_stream:
print(
"Dimensions", idims, "indicated",
"that there should be", math.prod(idims),
"elements.", "Instead, only ", elem_count,
"elements were found.",
file=string_stream
)
msg = string_stream.getvalue()
raise TypeError(msg)
def __getitem__(self, xkey):
ikey = tuple(map(int, iter(flatten(xkey))))
return self.d[ikey].data
def __setitem__(self, xkey, xval):
ikey = tuple(map(int, iter(flatten(xkey))))
self.d[ikey].data = xval
return
def get_column(self, coli):
coli = int(str(coli))
def get_row(self, rowi):
rowi = int(str(rowi))
def __mul__(left, right):
print("__mul__ was called")
raise NotImplementedError()
def __rmul__(right, left):
"""
m = Matrix([1, 2, 3])
88 * m
"""
print("__rmul__ was called")
raise NotImplementedError()
def __matmul__(left, right):
print("__MATMUL__ WAS CALLED")
raise NotImplementedError()
def __rmatmul__(right, left):
print(40*"#")
print("__rmatmul__")
print("left == ", left)
print("right == ", right)
print(40 * "#")
raise NotImplementedError()
def __imatmul__(total, step):
print("__imatmul__")
raise NotImplementedError()
def __str__(self):
raise NotImplementedError()
def __repr__(self):
return type(self) + str(self)
row_then_col = [
[[("r0", "c0")], [("r0", "c1")], [("r0", "c2")]],
[[("r1", "c0")], [("r1", "c1")], [("r1", "c2")]],
[[("r2", "c0")], [("r2", "c1")], [("r2", "c2")]],
]
a = Matrix([3, 3], row_then_col)
I'm trying to make a bunch of geometric objects which have their intrinsic geometric properties (center point, radius, lengths, etc.), as well as properties to help plot them (like x, y, z coordinates for a triangular mesh, arc resolution, etc.).
Since calculating the x, y, z coordinates is an expensive task for some of the shapes (like a triangular prism with edge rounding), I don't want to do it every time a property is changed, but only when the coordinates are requested. Even then though, it shouldn't be necessary to recalculate them if the shape's definition hasn't changed.
So my solution has been to create a "hash" which is is simply a tuple of all parameters which define the shape's "state." If the hash is unchanged, then the previously calculated coordinates can be re-used, otherwise, the coordinates must be recalculated. So I'm using the hash as a way to store the signature or fingerprint of the shape's definition.
I think what I have works, but I wonder if there are more robust ways to handle this that take advantage of __hash__ or id's or something. That feels like overkill to me, but I'm open to suggestions.
Here's my implementation for a sphere. I'm using Mayavi for plotting at the end, which you can skip/ignore if you don't have Mayavi.
#StdLib Imports
import os
#Numpy Imports
import numpy as np
from numpy import sin, cos, pi
class Sphere(object):
"""
Class for a sphere
"""
def __init__(self, c=None, r=None, n=None):
super(Sphere, self).__init__()
#Initial defaults
self._coordinates = None
self._c = np.array([0.0, 0.0, 0.0])
self._r = 1.0
self._n = 20
self._hash = []
#Assign Inputs
if c is not None:
self._c = c
if r is not None:
self._r = r
if n is not None:
self._n = n
#property
def c(self):
return self._c
#c.setter
def c(self, val):
self._c = val
#property
def r(self):
return self._r
#r.setter
def r(self, val):
self._r = val
#property
def n(self):
return self._n
#n.setter
def n(self, val):
self._n = val
#property
def coordinates(self):
self._lazy_update()
return self._coordinates
def _lazy_update(self):
new_hash = self._get_hash()
old_hash = self._hash
if new_hash != old_hash:
self._update_coordinates()
def _get_hash(self):
return tuple(map(tuple, [self._c, [self._r, self._n]]))
def _update_coordinates(self):
c, r, n = self._c, self._r, self._n
dphi, dtheta = pi / n, pi / n
[phi, theta] = np.mgrid[0:pi + dphi*1.0:dphi,
0:2*pi + dtheta*1.0:dtheta]
x = c[0] + r * cos(phi) * sin(theta)
y = c[1] + r * sin(phi) * sin(theta)
z = c[2] + r * cos(theta)
self._coordinates = x, y, z
self._hash = self._get_hash()
if __name__ == '__main__':
from mayavi import mlab
ns = [4, 6, 8, 10, 20, 50]
sphere = Sphere()
for i, n in enumerate(ns):
sphere.c = [i*2.2, 0.0, 0.0]
sphere.n = n
mlab.mesh(*sphere.coordinates, representation='wireframe')
mlab.show()
As suggested, here's a version that uses a dictionary to store the hash as a key:
#StdLib Imports
import os
#Numpy Imports
import numpy as np
from numpy import sin, cos, pi
class Sphere(object):
"""
Class for a sphere
"""
def __init__(self, c=None, r=None, n=None):
super(Sphere, self).__init__()
#Initial defaults
self._coordinates = {}
self._c = np.array([0.0, 0.0, 0.0])
self._r = 1.0
self._n = 20
#Assign Inputs
if c is not None:
self._c = c
if r is not None:
self._r = r
if n is not None:
self._n = n
#property
def c(self):
return self._c
#c.setter
def c(self, val):
self._c = val
#property
def r(self):
return self._r
#r.setter
def r(self, val):
self._r = val
#property
def n(self):
return self._n
#n.setter
def n(self, val):
self._n = val
#property
def _hash(self):
return tuple(map(tuple, [self._c, [self._r, self._n]]))
#property
def coordinates(self):
if self._hash not in self._coordinates:
self._update_coordinates()
return self._coordinates[self._hash]
def _update_coordinates(self):
c, r, n = self._c, self._r, self._n
dphi, dtheta = pi / n, pi / n
[phi, theta] = np.mgrid[0:pi + dphi*1.0:dphi,
0:2 * pi + dtheta*1.0:dtheta]
x = c[0] + r * cos(phi) * sin(theta)
y = c[1] + r * sin(phi) * sin(theta)
z = c[2] + r * cos(theta)
self._coordinates[self._hash] = x, y, z
if __name__ == '__main__':
from mayavi import mlab
ns = [4, 6, 8, 10, 20, 50]
sphere = Sphere()
for i, n in enumerate(ns):
sphere.c = [i*2.2, 0.0, 0.0]
sphere.n = n
mlab.mesh(*sphere.coordinates, representation='wireframe')
mlab.show()
Why not simply keep a flag:
class Sphere(object):
def __init__(self, ...):
...
self._update_coordinates()
...
#c.setter
def c(self, val):
self._changed = True
self._c = val
...
def _lazy_update(self):
if self._changed:
self._update_coordinates()
def _update_coordinates(self):
...
self._changed = False
The Problem
I recently found someone's awesome little pure-Python raytracing script from this link, and extended it a little bit for more convenient functions. However, sometimes it distorts the shapes of the objects and I'm wondering if someone with raytracing/3d experience might have any clue as to what might be causing it?
Some Info
The scene I'm testing with consists of a ground-level plane with three colored spheres placed on top of it. It produces good-looking scenes when the camera is looking down on the scene from above/at angles and at a certain distance (see the first two pics); however, when the camera gets closer to the ground level and closer to the objects the spheres end up changing their shapes and becoming oblong as if they're being stretched up towards the sky (see third pic). Note that the camera in the third pic with the distorted spheres is sort of upside down, which is because I'm still figuring out how to control the camera and not sure how to "spin it" upright when that happens; it seems to automatically look towards the general area where the spheres/light source is located, and only if I change some parameters will it look in different directions.
I'm still trying to decipher and understand what goes on in the original code that I found and based my code on, so I don't know but it could be something about the method or approach to raytracing taken by the original author. I've attached the entire code of my module script which should run when you press F5 if anyone is up for the challenge. The image rendering requires PIL, and if you want to play with the position of the camera, just look at the Camera class, and change its options in the "normaltest" function.
Update
Someone pointed out that when running the script it doesn't reproduce the problem in the third image. I have now changed the camera position for the normaltest function so that it will reproduce the problem (see the new fourth image for how it should look like). In case you're wondering why the light seems to be shooting out of the spheres it's bc I placed the lightsource somewhere in between all of them.
Im starting to think that the problem is with the camera and me not understanding it completely.
The camera options zoom, xangle, and yangle may not do what their names imply; that's just how I named them based on what they seemed to do when I changed them up. Originally they were not variables but rather some constant nr in a calculation that had to be changed manually. Specifically they are used to define and produce the rays through the scene on line 218 in the renderScene function.
For instance, sometimes when I change the zoom value it also changes the direction and position of the camera.
It's a bit odd that in the original code the camera was just defined as a point with no direction (the xangle and yangle variables were at first just static nrs with no option for defining them), and almost always starts out looking towards the object automatically.
I cant find a way to "spin"/tilt the camera around itself.
Try also to heighten the camera from its current z-coordinate of 2 to a z of 5, a very small change but it makes the distortion dramatically better looking (though still bad), so proximity to the ground or the shift in angle that comes with it seems to play some role.
"""
Pure Python ray-tracer :)
taken directly from http://pastebin.com/f8f5ghjz with modifications
another good one alternative at http://www.hxa.name/minilight/
some more equations for getting intersection with other 3d geometries, https://www.cl.cam.ac.uk/teaching/1999/AGraphHCI/SMAG/node2.html#SECTION00023200000000000000
"""
#IMPORTS
from math import sqrt, pow, pi
import time
import PIL,PIL.Image
#GEOMETRIES
class Vector( object ):
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
def dot(self, b):
return self.x*b.x + self.y*b.y + self.z*b.z
def cross(self, b):
return (self.y*b.z-self.z*b.y, self.z*b.x-self.x*b.z, self.x*b.y-self.y*b.x)
def magnitude(self):
return sqrt(self.x**2+self.y**2+self.z**2)
def normal(self):
mag = self.magnitude()
return Vector(self.x/mag,self.y/mag,self.z/mag)
def __add__(self, b):
return Vector(self.x + b.x, self.y+b.y, self.z+b.z)
def __sub__(self, b):
return Vector(self.x-b.x, self.y-b.y, self.z-b.z)
def __mul__(self, b):
assert type(b) == float or type(b) == int
return Vector(self.x*b, self.y*b, self.z*b)
class Sphere( object ):
def __init__(self, center, radius, color):
self.c = center
self.r = radius
self.col = color
def intersection(self, l):
q = l.d.dot(l.o - self.c)**2 - (l.o - self.c).dot(l.o - self.c) + self.r**2
if q < 0:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
else:
d = -l.d.dot(l.o - self.c)
d1 = d - sqrt(q)
d2 = d + sqrt(q)
if 0 < d1 and ( d1 < d2 or d2 < 0):
return Intersection(l.o+l.d*d1, d1, self.normal(l.o+l.d*d1), self)
elif 0 < d2 and ( d2 < d1 or d1 < 0):
return Intersection(l.o+l.d*d2, d2, self.normal(l.o+l.d*d2), self)
else:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
def normal(self, b):
return (b - self.c).normal()
class Cylinder( object ):
"just a copy of sphere, needs work. maybe see http://stackoverflow.com/questions/4078401/trying-to-optimize-line-vs-cylinder-intersection"
def __init__(self, startpoint, endpoint, radius, color):
self.s = startpoint
self.e = endpoint
self.r = radius
self.col = color
def intersection(self, l):
q = l.d.dot(l.o - self.c)**2 - (l.o - self.c).dot(l.o - self.c) + self.r**2
if q < 0:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
else:
d = -l.d.dot(l.o - self.c)
d1 = d - sqrt(q)
d2 = d + sqrt(q)
if 0 < d1 and ( d1 < d2 or d2 < 0):
return Intersection(l.o+l.d*d1, d1, self.normal(l.o+l.d*d1), self)
elif 0 < d2 and ( d2 < d1 or d1 < 0):
return Intersection(l.o+l.d*d2, d2, self.normal(l.o+l.d*d2), self)
else:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
def normal(self, b):
return (b - self.c).normal()
class LightBulb( Sphere ):
pass
class Plane( object ):
"infinite, no endings"
def __init__(self, point, normal, color):
self.n = normal
self.p = point
self.col = color
def intersection(self, l):
d = l.d.dot(self.n)
if d == 0:
return Intersection( vector(0,0,0), -1, vector(0,0,0), self)
else:
d = (self.p - l.o).dot(self.n) / d
return Intersection(l.o+l.d*d, d, self.n, self)
class Rectangle( object ):
"not done. like a plane, but is limited to the shape of a defined rectangle"
def __init__(self, point, normal, color):
self.n = normal
self.p = point
self.col = color
def intersection(self, ray):
desti = ray.dest.dot(self.n)
if desti == 0:
#??
return Intersection( vector(0,0,0), -1, vector(0,0,0), self)
else:
desti = (self.p - ray.orig).dot(self.n) / desti
return Intersection(ray.orig+ray.desti*desti, desti, self.n, self)
class RectangleBox( object ):
"not done. consists of multiple rectangle objects as its sides"
pass
class AnimatedObject( object ):
def __init__(self, *objs):
self.objs = objs
def __iter__(self):
for obj in self.objs:
yield obj
def __getitem__(self, index):
return self.objs[index]
def reverse(self):
self.objs = [each for each in reversed(self.objs)]
return self
#RAY TRACING INTERNAL COMPONENTS
class Ray( object ):
def __init__(self, origin, direction):
self.o = origin
self.d = direction
class Intersection( object ):
"keeps a record of a known intersection bw ray and obj?"
def __init__(self, point, distance, normal, obj):
self.p = point
self.d = distance
self.n = normal
self.obj = obj
def testRay(ray, objects, ignore=None):
intersect = Intersection( Vector(0,0,0), -1, Vector(0,0,0), None)
for obj in objects:
if obj is not ignore:
currentIntersect = obj.intersection(ray)
if currentIntersect.d > 0 and intersect.d < 0:
intersect = currentIntersect
elif 0 < currentIntersect.d < intersect.d:
intersect = currentIntersect
return intersect
def trace(ray, objects, light, maxRecur):
if maxRecur < 0:
return (0,0,0)
intersect = testRay(ray, objects)
if intersect.d == -1:
col = vector(AMBIENT,AMBIENT,AMBIENT)
elif intersect.n.dot(light - intersect.p) < 0:
col = intersect.obj.col * AMBIENT
else:
lightRay = Ray(intersect.p, (light-intersect.p).normal())
if testRay(lightRay, objects, intersect.obj).d == -1:
lightIntensity = 1000.0/(4*pi*(light-intersect.p).magnitude()**2)
col = intersect.obj.col * max(intersect.n.normal().dot((light - intersect.p).normal()*lightIntensity), AMBIENT)
else:
col = intersect.obj.col * AMBIENT
return col
def gammaCorrection(color,factor):
return (int(pow(color.x/255.0,factor)*255),
int(pow(color.y/255.0,factor)*255),
int(pow(color.z/255.0,factor)*255))
#USER FUNCTIONS
class Camera:
def __init__(self, cameraPos, zoom=50.0, xangle=-5, yangle=-5):
self.pos = cameraPos
self.zoom = zoom
self.xangle = xangle
self.yangle = yangle
def renderScene(camera, lightSource, objs, imagedims, savepath):
imgwidth,imgheight = imagedims
img = PIL.Image.new("RGB",imagedims)
#objs.append( LightBulb(lightSource, 0.2, Vector(*white)) )
print "rendering 3D scene"
t=time.clock()
for x in xrange(imgwidth):
#print x
for y in xrange(imgheight):
ray = Ray( camera.pos, (Vector(x/camera.zoom+camera.xangle,y/camera.zoom+camera.yangle,0)-camera.pos).normal())
col = trace(ray, objs, lightSource, 10)
img.putpixel((x,imgheight-1-y),gammaCorrection(col,GAMMA_CORRECTION))
print "time taken", time.clock()-t
img.save(savepath)
def renderAnimation(camera, lightSource, staticobjs, animobjs, imagedims, savepath, saveformat):
"NOTE: savepath should not have file extension, but saveformat should have a dot"
time = 0
while True:
print "time",time
timesavepath = savepath+"_"+str(time)+saveformat
objs = []
objs.extend(staticobjs)
objs.extend([animobj[time] for animobj in animobjs])
renderScene(camera, lightSource, objs, imagedims, timesavepath)
time += 1
#SOME LIGHTNING OPTIONS
AMBIENT = 0.05 #daylight/nighttime
GAMMA_CORRECTION = 1/2.2 #lightsource strength?
#COLORS
red = (255,0,0)
yellow = (255,255,0)
green = (0,255,0)
blue = (0,0,255)
grey = (120,120,120)
white = (255,255,255)
purple = (200,0,200)
def origtest():
print ""
print "origtest"
#BUILD THE SCENE
imagedims = (500,500)
savepath = "3dscene_orig.png"
objs = []
objs.append(Sphere( Vector(-2,0,-10), 2, Vector(*green)))
objs.append(Sphere( Vector(2,0,-10), 3.5, Vector(*red)))
objs.append(Sphere( Vector(0,-4,-10), 3, Vector(*blue)))
objs.append(Plane( Vector(0,0,-12), Vector(0,0,1), Vector(*grey)))
lightSource = Vector(0,10,0)
camera = Camera(Vector(0,0,20))
#RENDER
renderScene(camera, lightSource, objs, imagedims, savepath)
def normaltest():
print ""
print "normaltest"
#BUILD THE SCENE
"""
the camera is looking down on the surface with the spheres from above
the surface is like looking down on the xy axis of the xyz coordinate system
the light is down there together with the spheres, except from one of the sides
"""
imagedims = (200,200)
savepath = "3dscene.png"
objs = []
objs.append(Sphere( Vector(-4, -2, 1), 1, Vector(*red)))
objs.append(Sphere( Vector(-2, -2, 1), 1, Vector(*blue)))
objs.append(Sphere( Vector(-2, -4, 1), 1, Vector(*green)))
objs.append(Plane( Vector(0,0,0), Vector(0,0,1), Vector(*grey)))
lightSource = Vector(-2.4, -3, 2)
camera = Camera(Vector(-19,-19,2), zoom=2.0, xangle=-30, yangle=-30)
#RENDER
renderScene(camera, lightSource, objs, imagedims, savepath)
def animtest():
print ""
print "falling ball test"
#BUILD THE SCENE
imagedims = (200,200)
savepath = "3d_fallball"
saveformat = ".png"
staticobjs = []
staticobjs.append(Sphere( Vector(-4, -2, 1), 1, Vector(*red)))
staticobjs.append(Sphere( Vector(-2, -4, 1), 1, Vector(*green)))
staticobjs.append(Plane( Vector(0,0,0), Vector(0,0,1), Vector(*purple)))
animobjs = []
fallingball = AnimatedObject(Sphere( Vector(-2, -2, 20), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 15), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 9), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 5), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 1), 1, Vector(*yellow)))
animobjs.append(fallingball)
lightSource = Vector(-4,-4,10)
camera = Camera(Vector(0,0,30))
#RENDER
renderAnimation(camera, lightSource, staticobjs, animobjs, imagedims, savepath, saveformat)
#RUN TESTS
#origtest()
normaltest()
#animtest()
The key to your problems is probably this line:
ray = Ray( camera.pos,
(Vector(
x/camera.zoom+camera.xangle,
y/camera.zoom+camera.yangle,
0)
-camera.pos)
.normal())
A ray is defined as a line going from camera position (however is that point defined) over XY plane, that is zoomed and SHIFTED by the xangle and yangle parameters.
This is not how perspective projection is usually implemented. This is more like a tilt/shift camera. A typical perspective transform would keep the plane you project onto PERPENDICULAR to the ray going from the camera through the centre of the picture.
With this code you have two options: either rewrite this, or always use xangle, yangle, camera.pos.x and camera.pos.y == 0. Otherwise you get wonky results.
To be correct, this is perfectly legit perspective. It is just not what you would ever see with a typical camera.