衝突シミュレータ3D版。

やっぱりどうみても衝突時の計算式が少しあやしい気がする。
左クリックでボールの生成、右ドラッグアンドドロップで衝突用のボールを(ドラッグアンドドロップ中だけ)生成。
下のスライダはフレームの軸回転、ビューの遠さの調整。

#!/usr/bin/env python

import Tkinter
from math import *

class boundBall3D:
    def __init__(self):
        self.window = Tkinter.Tk()
        self.width = 800
        self.height = 600
        self.canvas = Tkinter.Canvas(self.window,bg="white",
                                     width=self.width,height=self.height)
        self.canvas.bind('<ButtonPress-1>',self.canvasPress1)
        self.canvas.bind('<ButtonPress-3>',self.canvasPress3)
        self.canvas.bind('<ButtonRelease-3>',self.canvasRelease3)
        self.canvas.bind('<Button3-Motion>',self.canvasMotion3)
        self.canvas.bind('<Motion>',self.canvasMotion)
        self.canvas.pack()
        self.xAxisRollAngle = Tkinter.Scale(self.window, from_=0, to=360,
                                            orient=Tkinter.HORIZONTAL)
        self.xAxisRollAngle.bind("<ButtonPress-1>",self.angleChanged)
        self.xAxisRollAngle.bind("<ButtonRelease-1>",self.angleChanged)
        self.xAxisRollAngle.bind("<B1-Motion>",self.angleChanged)
        self.xAxisRollAngle.pack(side=Tkinter.LEFT);

        self.yAxisRollAngle = Tkinter.Scale(self.window, from_=0, to=360,
                                            orient=Tkinter.HORIZONTAL)
        self.yAxisRollAngle.bind("<ButtonPress-1>",self.angleChanged)
        self.yAxisRollAngle.bind("<ButtonRelease-1>",self.angleChanged)
        self.yAxisRollAngle.bind("<B1-Motion>",self.angleChanged)
        self.yAxisRollAngle.pack(side=Tkinter.LEFT);

        self.zAxisRollAngle = Tkinter.Scale(self.window, from_=0, to=360,
                                            orient=Tkinter.HORIZONTAL)
        self.zAxisRollAngle.bind("<ButtonRelease-1>",self.angleChanged)
        self.zAxisRollAngle.bind("<ButtonPress-1>",self.angleChanged)
        self.zAxisRollAngle.bind("<B1-Motion>",self.angleChanged)
        self.zAxisRollAngle.pack(side=Tkinter.LEFT);

        self.viewPoint = Tkinter.Scale(self.window, from_=10, to=1000,
                                       orient=Tkinter.HORIZONTAL)
        self.viewPoint.bind("<ButtonRelease-1>",self.angleChanged)
        self.viewPoint.bind("<ButtonPress-1>",self.angleChanged)
        self.viewPoint.bind("<B1-Motion>",self.angleChanged)
        self.viewPoint.set(100)
        self.viewPoint.pack(side=Tkinter.LEFT);

        self.objects = []

        self.createObject(['line',-200,200,-200,200,200,-200])
        self.createObject(['line',200,200,-200,200,-200,-200])
        self.createObject(['line',200,-200,-200,-200,-200,-200])
        self.createObject(['line',-200,-200,-200,-200,200,-200])

        self.createObject(['line',-200,200,-200,-200,200,200,])
        self.createObject(['line',200,200,-200,200,200,200,])
        self.createObject(['line',200,-200,-200,200,-200,200,])
        self.createObject(['line',-200,-200,-200,-200,-200,200,])

        self.createObject(['line',-200,200,200,200,200,200])
        self.createObject(['line',200,200,200,200,-200,200])
        self.createObject(['line',200,-200,200,-200,-200,200])
        self.createObject(['line',-200,-200,200,-200,200,200])
        self.moving = 0
        self.dragObject = None
        self.window.mainloop()

    def calcInterval(self,x1,y1,z1,x2,y2,z2):
        return pow(fabs(pow(x1-x2,3))+fabs(pow(y1-y2,3))+fabs(pow(z1-z2,3)),1/3.0)

    def conflictSpheres(self,sphere):
        return [o for o in self.objects
                if o['itemType']=='sphere'
                and self.calcInterval(sphere['coords'][0],
                                      sphere['coords'][1],
                                      sphere['coords'][2],
                                      o['coords'][0],
                                      o['coords'][1],
                                      o['coords'][2],
                                      ) <= sphere['coords'][3]+o['coords'][3]
                and sphere['id']!=o['id']]

    def calcConflict(self,x1,y1,vx1,vy1,x2,y2,vx2,vy2):
        v2rad = atan2(vy2,vx2)
        v2size = pow(pow(x2,2)+pow(y2,2),0.5)
        rad2to1 = atan2(y1-y2,x1-x2)
        rad = v2rad - rad2to1
        vcos = pow(cos(rad),2)
        vx,vy = cos(rad2to1)*vcos,sin(rad2to1)*vcos
        return vx,vy

    def afterImageEffect(self,obj,color):
        if color<=0:
            self.canvas.delete(obj['id'])
            self.objects.remove(obj)
            return
        c = '#%02x%02x%02x'%(255,255-color,255-color)
        self.canvas.itemconfigure(obj['id'],fill=c,outline=c)
        self.window.after(100,self.afterImageEffect,obj,color-64)

    def moveObjects(self):
        for obj in [o for o in self.objects if 'vector' in o]:
            if self.dragObject!=None and obj['id']==self.dragObject['id']: continue
            if obj['itemType']=='sphere':
                [x,y,z,r],[vx,vy,vz] = obj['coords'],obj['vector']
                shadowId = self.createObject(['shadow',x,y,z,r])
                self.afterImageEffect(shadowId,255)
                x,y,z = x+vx,y+vy,z+vz
                if x < -200+r or x > 200-r:
                    vx = -vx
                if y < -200+r or y > 200-r:
                    vy = -vy
                if z < -200+r or z > 200-r:
                    vz = -vz
                obj['coords'] = [x,y,z,r]
                obj['vector'] = [vx,vy,vz]
                for conflict in self.conflictSpheres(obj):
                    [bx,by,bz,br],[vbx,vby,vbz] = conflict['coords'],conflict['vector']
                    cvx1,cvx2 = self.calcConflict(y,z,vy,vz,by,bz,vby,vbz)
                    cvy1,cvy2 = self.calcConflict(x,z,vx,vz,bx,bz,vbx,vbz)
                    cvz1,cvz2 = self.calcConflict(x,y,vx,vy,bx,by,vbx,vby)
                    obj['vector'] = [vx+(cvy1+cvz1),vy+(cvx1+cvz2),vz+(cvx2+cvy2)]
                    conflict['vector'] = [vbx-(cvy1+cvz1),vby-(cvx1+cvz2),vbz-(cvx2+cvy2)]
                itemPositions = self.calcObjectPosition(obj)
                self.canvas.coords(obj['id'],*itemPositions)
        self.window.after(100,self.moveObjects)

    def canvasPress1(self,event):
        if self.moving==0:
            self.moveObjects()
            self.moving = 1
        (x,y,z) = event.x,event.y,0
        (x,y,z) = self.cnvMap(x,y)
        (x,y,z) = self.roll3DbySettingReverse(x,y,z)
        r = 10
        if -200+r < x < 200-r and -200+r < y < 200-r and -200+r < z < 200-r:
            obj = self.createObject(['sphere',x,y,z,r])

    def canvasPress3(self,event):
        (x,y,z) = self.roll3DbySettingReverse(*self.cnvMap(event.x,event.y))
        self.dragObject = self.createObject(['sphere',x,y,z,10])

    def canvasMotion3(self,event):
        (x,y,z) = self.roll3DbySettingReverse(*self.cnvMap(event.x,event.y))
        self.dragObject['coords'][0] = x
        self.dragObject['coords'][1] = y
        self.dragObject['coords'][2] = z
        itemPositions = self.calcObjectPosition(self.dragObject)
        self.canvas.coords(self.dragObject['id'],*itemPositions)

    def canvasRelease3(self,event):
        self.canvas.delete(self.dragObject['id'])
        self.objects.remove(self.dragObject)
        self.dragObject = None

    def canvasMotion(self,event):
#        (x,y,z) = self.roll3DbySettingReverse(*self.cnvMap(event.x,event.y))
#        print x,y,z
        pass

    def roll(self,x,y,degree):
        (orgX,orgY) = (x,y)
        orgR = sqrt(pow(orgX,2)+pow(orgY,2))
        if orgR==0:
            return (x,y)
        orgTheta = atan2(orgY/orgR,orgX/orgR)
        theta = radians(degree)+orgTheta
        x = cos(theta)*orgR
        y = sin(theta)*orgR
        return (x,y)

    def calcObjectPosition(self,object):
        if object['itemType']=='line':
            item,itemType,[sx,sy,sz,ex,ey,ez] = object['id'],object['itemType'],object['coords']
            (rsx,rsy,rsz) = self.roll3DbySetting(sx,sy,sz)
            (rex,rey,rez) = self.roll3DbySetting(ex,ey,ez)
            (lineSx,lineSy) = self.cnvCanvas(rsx,rsy,rsz)
            (lineEx,lineEy) = self.cnvCanvas(rex,rey,rez)
            return (lineSx,lineSy,lineEx,lineEy)
        if object['itemType']=='sphere' or object['itemType']=='shadow':
            item,itemType,[x,y,z,r] = object['id'],object['itemType'],object['coords']
            (x,y,z) = self.roll3DbySetting(x,y,z)
            sx,sy =self.cnvCanvas(x,y,z)
            return (sx-r,sy-r,sx+r,sy+r)

    def angleChanged(self,event):
        for ii in range(len(self.objects)):
            itemPositions = self.calcObjectPosition(self.objects[ii])
            self.canvas.coords(self.objects[ii]['id'],*itemPositions)
            
    def roll3DbySetting(self,x,y,z):
        x,y = self.roll(x,y,self.zAxisRollAngle.get())
        y,z = self.roll(y,z,self.xAxisRollAngle.get())
        x,z = self.roll(x,z,self.yAxisRollAngle.get())
        return (x,y,z)

    def roll3DbySettingReverse(self,x,y,z):
        x,z = self.roll(x,z,-self.yAxisRollAngle.get())
        y,z = self.roll(y,z,-self.xAxisRollAngle.get())
        x,y = self.roll(x,y,-self.zAxisRollAngle.get())
        return (x,y,z)

    def cnvMap(self,x,y):
        return (x-self.width/2,(y-self.height/2)*-1,0)

    def cnvCanvas(self,x,y,z):
        interval = self.viewPoint.get()*100 / (self.viewPoint.get()*100 + float(z))
        x = x*interval
        y = y*interval
        return (x+self.width/2,(y-self.height/2)*-1)

    def createObject(self,item):
        if item[0]=='line':
            (itemType,sx,sy,sz,ex,ey,ez) = (item[0],
                                            int(item[1]),int(item[2]),int(item[3]),
                                            int(item[4]),int(item[5]),int(item[6]))
            (lineSx,lineSy,lineEx,lineEy) = self.calcObjectPosition(
                dict(id=-1,itemType=itemType,coords=[sx,sy,sz,ex,ey,ez]))
            line = self.canvas.create_line(lineSx,lineSy,
                                           lineEx,lineEy,
                                           fill="red",width=3)
            obj = dict(id=line,itemType=itemType,coords=[sx,sy,sz,ex,ey,ez],vector=[0,0,0])
            self.objects.append(obj)
            return obj
        elif item[0]=='sphere' or item[0]=='shadow':
            (itemType,x,y,z,r) = (item[0],int(item[1]),int(item[2]),int(item[3]),int(item[4]))
            (x1,y1,x2,y2) = self.calcObjectPosition(
                dict(id=-1,itemType=itemType,coords=[x,y,z,r]))
            sphere = self.canvas.create_oval(x1,y1,x2,y2,fill='red',outline='red')
            obj = dict(id=sphere,itemType=itemType,coords=[x,y,z,r],vector=[0,0,0])
            self.objects.append(obj)
            return obj
        else:
            pass
        pass
if __name__ == '__main__':
    boundBall3D()