• Facebook
  • Twitter
  • Reddit
  • StumbleUpon
  • Digg
  • email

#####################################################################################
#
#  Copyright (c) Microsoft Corporation. All rights reserved.
#
# This source code is subject to terms and conditions of the Apache License, Version 2.0. A 
# copy of the license can be found in the License.html file at the root of this distribution. If 
# you cannot locate the  Apache License, Version 2.0, please send an email to 
# ironpy@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
# by the terms of the Apache License, Version 2.0.
#
# You must not remove this notice, or any other, from this software.
#
#
#####################################################################################
 
# This file is an updated version of the framework which is built during
# the course of the tutorial.  While most of it remains the same, I have
# updated a few functions, added a few new ones, and added documentation
# throughout the file.  If you are looking for an exact copy of what would
# you should end up with after working through the tutorial, find
# checkpoint6.py, which should have been included with this tutorial.
 
import clr
clr.AddReferenceByPartialName("System.Drawing")
clr.AddReferenceByPartialName("System.Windows.Forms")
clr.AddReferenceByPartialName("Microsoft.DirectX")
clr.AddReferenceByPartialName("Microsoft.DirectX.Direct3D")
clr.AddReferenceByPartialName("Microsoft.DirectX.Direct3DX")
 
import operator
import math
import thread
import System
from System import Drawing
from System.Windows import Forms
from Microsoft import DirectX
from Microsoft.DirectX import Direct3D
 
single = System.Single
 
def Degree(degrees):
    """All functions take in radians, so to use degrees, you must pass it
    through this conversion function."""
    return (math.pi * degrees) / 180.0
 
 
def Radian(radians):
    """All functions take in radians, so this function does nothing...other
    than explicitly state that you are using radians."""
    return radians
 
 
def Vectorize(v):
    "Converts v from a sequence into a Vector3."
    if operator.isSequenceType(v):
        v = DirectX.Vector3(System.Single(v[0]), System.Single(v[1]), System.Single(v[2]))
 
    return v
 
 
def GetFunctionName(f):
    "Creates a formatted function string for display."
    try:
        name = f.__name__
        if hasattr(f, "im_class"):
            name = f.im_class.__name__ + "." + name
        return name
    except:
        # We do NOT want to throw an exception from this function...
        # If anything goes wrong we just want to quietly return a string.
        return ""
 
 
def HandleException(desc, exception):
    """This pops up Windows.Forms message box in a seperate thread
    containing the exception that occurred.  This framework tries its
    best to avoid exceptions by removing the offending listener/scene
    object whenever it throws an exception.  This may not be the best
    way to handle exceptions if this were used as a full application,
    but it does a reasonably good job of keeping the framework running
    (and not dieing due to an unhandled exception)."""
    args = str(desc) + "\n" + str(exception), "An exception occurred!"
    thread.start_new_thread(Forms.MessageBox.Show, args)
 
 
# Hack: I wanted this tutorial to run under IronPython without having to install
#       CPython along side it.  This code should really be replaced with a
#       reference to the types module in CPython:
#           from types import ClassType
class _TestClass: pass
ClassType = type(_TestClass)
del _TestClass
 
 
class RotatableObject(object):
    "Functions for rotating objects.  All units are in radians."
    def __init__(self):
        self.RotationMatrix = DirectX.Matrix.Identity
 
    def ResetOrientation(self):
        self.RotationMatrix = DirectX.Matrix.Identity
 
    def Pitch(self, p):
        "Rotation around the X axis"
        self.RotationMatrix *= DirectX.Matrix.RotationX(p)
 
    def Yaw(self, y):
        "Rotation around the Y axis"
        self.RotationMatrix *= DirectX.Matrix.RotationY(y)
 
    def Roll(self, r):
        "Rotation around the Z axis"
        self.RotationMatrix *= DirectX.Matrix.RotationZ(r)
 
 
class PositionableObject(object):
    "Functions for positioning objects."
    def __init__(self):
        self.__Position = DirectX.Vector3(0, 0, 0)
        self.PositionMatrix = DirectX.Matrix.Identity
 
    def __GetPosition(self):
        return self.__Position
 
    def __SetPosition(self, pos):
        pos = Vectorize(pos)
        self.__Position = pos
        self.PositionMatrix = DirectX.Matrix.Translation(self.__Position)
 
    def __DelPosition(self):
        self.__Position = DirectX.Vector3(0, 0, 0)
        self.PositionMatrix = DirectX.Matrix.Identity
 
    def Translate(self, amount):
        amount = Vectorize(amount)
        self.__Position += amount
        self.PositionMatrix = DirectX.Matrix.Translation(self.__Position)
 
    Position = property(__GetPosition, __SetPosition, __DelPosition)
 
 
class SceneObject(PositionableObject, RotatableObject):
    "A combined position/rotation class for easy access."
    def __init__(self):
        self.Matrix = DirectX.Matrix.Identity
        self.__PositionMatrix = DirectX.Matrix.Identity
        self.__RotationMatrix = DirectX.Matrix.Identity
        self.Invert = False
 
        PositionableObject.__init__(self)
        RotatableObject.__init__(self)
 
    def __GetPositionMatrix(self):
        return self.__PositionMatrix
 
    def __SetPositionMatrix(self, value):
        self.__PositionMatrix = value
        self.Matrix = self.__RotationMatrix * self.__PositionMatrix
        if self.Invert:
            self.Matrix.Invert()
 
    def __GetRotationMatrix(self):
        return self.__RotationMatrix
 
    def __SetRotationMatrix(self, value):
        self.__RotationMatrix = value
        self.Matrix = self.__RotationMatrix * self.__PositionMatrix
        if self.Invert:
            self.Matrix.Invert()
 
    PositionMatrix = property(__GetPositionMatrix, __SetPositionMatrix)
    RotationMatrix = property(__GetRotationMatrix, __SetRotationMatrix)
 
 
class Camera(SceneObject):
    def __init__(self, name):
        SceneObject.__init__(self)
        self.Name = name
        self.Invert = True
        self.LookAtMatrix = DirectX.Matrix.Identity
 
    def LookAt(self, position, up=DirectX.Vector3(0, 1, 0)):
        """Takes in a position to look at and an up vector to use to look at
        it with.  The up vector will normally be the positive Y axis, but in
        the case where the position is directly above or below the position
        of the camera, you will need to set which direction up is in this
        case."""
        zero = DirectX.Vector3(0, 0, 0)
        direction = Vectorize(position) - self.Position
        up = Vectorize(up)
        self.LookAtMatrix = DirectX.Matrix.LookAtLH(zero, direction, up)
        self.RotationMatrix = DirectX.Matrix.Identity
 
    def ResetOrientation(self):
        self.RotationMatrix = DirectX.Matrix.Identity
        self.LookAtMatrix = DirectX.Matrix.Identity
 
    def GetViewMatrix(self):
        return self.Matrix * self.LookAtMatrix
 
 
class MeshRenderable(SceneObject):
    "A scene object which represents a mesh."
    def __init__(self, device, name, file):
        SceneObject.__init__(self)
        self.Name = name
        materials = clr.Reference[System.Array[Direct3D.ExtendedMaterial]](())
        self.Mesh = Direct3D.Mesh.FromFile(file,
                                           Direct3D.MeshFlags.SystemMemory,
                                           device, materials)
        materials = materials.Value
 
        self.Materials = []
        self.Textures = []
        for i in range(materials.Length):
            # load material, set color
            material = materials[i].Material3D
            material.AmbientColor = material.DiffuseColor
 
            # load texture, if possible
            texture = None
            texFile = materials[i].TextureFilename
            if texFile is not None and texFile.Length:
                texture = Direct3D.TextureLoader.FromFile(device, texFile)
 
            # insert the material and texture into the lists
            self.Materials.append(material)
            self.Textures.append(texture)
 
    def GetWorldMatrix(self):
        return self.Matrix
 
 
class BasicObject(SceneObject):
    "A scene object for basic meshes which are provided by DirectX"
    ObjectTypes = {'cylinder' : Direct3D.Mesh.Cylinder,
                   'polygon' : Direct3D.Mesh.Polygon,
                   'sphere' : Direct3D.Mesh.Sphere,
                   'teapot' : Direct3D.Mesh.Teapot,
                   'torus' : Direct3D.Mesh.Torus,
                   'box' : Direct3D.Mesh.Box
                   }
    def __init__(self, device, type, name, color, *params):
        SceneObject.__init__(self)
        if not callable(type):
            type = self.ObjectTypes[str(type).lower()]
        self.Name = name
        self.Mesh = type(device, *params)
 
        color = Direct3D.ColorValue.FromColor(color)
        material = Direct3D.Material()
        material.AmbientColor = color
 
        self.Materials = [material]
        self.Textures = []
 
    def GetWorldMatrix(self):
        return self.Matrix
 
 
class RenderWindow(Forms.Form):
    """A simple Forms window to host the app.  This should really be changed to
    allow for different resolutions."""
    def __init__(self, sceneManager):
        self.SceneManager = sceneManager
        self.Text = "IronPython Direct3D"
        self.ClientSize = Drawing.Size(640, 480)
        self.InputManager = None
 
    def OnKeyDown(self, args):
        if self.InputManager is not None:
            self.InputManager.InjectKeyUp(args.KeyCode)
 
    def OnKeyUp(self, args):
        if self.InputManager is not None:
            self.InputManager.InjectKeyDown(args.KeyCode)
 
    def OnKeyPress(self, args):
        if self.InputManager is not None:
            self.InputManager.InjectKey(args.KeyChar)
 
 
    def OnResize(self, args):
        self.SceneManager.Paused = not self.Visible or \
            (self.WindowState == Forms.FormWindowState.Minimized)
 
class InputManager(object):
    def __init__(self):
        self.KeyListeners = {'OnKeyDown' : [],
                             'OnKeyUp' : [],
                             'OnKey' : []
                             }
 
    def AddEventListener(self, event, listener):
        if not callable(listener):
            raise TypeError, "listener must be callable"
        self.KeyListeners[event].Add(listener)
 
    def AddListener(self, listener):
        if operator.isSequenceType(listener):
            for obj in listener:
                self.AddKeyListener(obj)
 
        else:
            if issubclass(type(listener), type) or type(listener) == ClassType:
                listener = listener()
 
            for key in self.KeyListeners.Keys:
                if hasattr(listener, key):
                    self.KeyListeners[key].Add(getattr(listener, key))
 
 
    def RemoveListener(self, listener):
        if operator.isSequenceType(listener):
            for obj in listener:
                self.RemoveKeyListener(obj)
 
        else:
            if issubclass(type(listener), type) or type(listener) == ClassType:
                for l in self.Listeners.Values:
                    for func in l:
                        if func.im_class == listener:
                            l.Remove(func)
 
        for f in self.Listeners[event]:
            if f.im_class == type:
                self.Listeners[event].Remove(f)
            else:
                for key in self.KeyListeners.Keys:
                    if hasattr(listener, key):
                        self.KeyListeners[key].Remove(getattr(listener, key))
 
    def __FireKeyEvent(self, seq, key):
        for f in seq:
            remove = False
            try:
                remove = f(key)
            except Exception, e:
                HandleException("An exception occurred while calling the KeyListener: " + GetFunctionName(f) + ".", e)
                remove = True
 
            if remove:
                try:
                    seq.Remove(f)
                except:
                    pass
 
    def InjectKey(self, key):
        self.__FireKeyEvent(self.KeyListeners['OnKey'], key)
 
    def InjectKeyDown(self, key):
        self.__FireKeyEvent(self.KeyListeners['OnKeyUp'], key)
 
    def InjectKeyUp(self, key):
        self.__FireKeyEvent(self.KeyListeners['OnKeyDown'], key)
 
class SceneManager(object):
    "The class which manages and renders all objects in the scene."
    def __init__(self):
        self.Device = None
        self.Paused = False
        self.Background = System.Drawing.Color.Black
        self.ActiveCamera = None
        self.Objects = {}
        self.Listeners = {"OnFrame" : [],
                          "OnSceneBegin" : [],
                          "OnSceneCreate" : []}
 
    def LoadBasicObject(self, type, name, color, *args):
        "Creates a basic object from the given paramters."
        mesh = BasicObject(self.Device, type, name, color, *args)
        self.Objects[mesh.Name] = mesh
        return mesh
 
 
 
    def LoadMesh(self, name, filename):
        "Loads a mesh and places it into an object based on the filename."
        mesh = MeshRenderable(self.Device, name, filename)
        self.Objects[mesh.Name] = mesh
        return mesh
 
 
 
    def CreateCamera(self, name):
        "Creates a Camera with the given name."
        cam = Camera(name)
        self.Objects[cam.Name] = cam
 
        if self.ActiveCamera is None:
            self.ActiveCamera = cam
 
        return cam
 
 
    def InitGraphics(self, handle):
        """Creates the Direct3D device which is used to render the scene.  Pops
        up a message box and returns False on failure (returns True on success).
        """
        params = Direct3D.PresentParameters()
        params.Windowed = True
        params.SwapEffect = Direct3D.SwapEffect.Discard
        params.EnableAutoDepthStencil = True
        params.AutoDepthStencilFormat = Direct3D.DepthFormat.D16
 
        self.Device = Direct3D.Device(0, Direct3D.DeviceType.Hardware, handle,
                                      Direct3D.CreateFlags.SoftwareVertexProcessing,
                                      params)
 
        self.Device.RenderState.ZBufferEnable = True
        self.Device.Transform.Projection = DirectX.Matrix.PerspectiveFovLH(System.Math.PI/4.0, 1, 1, 100)
        self.Device.Transform.View = DirectX.Matrix.LookAtLH(DirectX.Vector3(0, 3, -5), DirectX.Vector3(0, 0, 0), DirectX.Vector3(0, 1, 0))
        self.Device.RenderState.Ambient = Drawing.Color.White
 
        # ensure we are not paused
        self.Paused = False
 
        return True
 
    def Render(self):
        if self.Device is None or self.Paused:
            return
 
        if self.ActiveCamera is not None:
            self.Device.Transform.View = self.ActiveCamera.GetViewMatrix()
 
 
        self.Device.Clear(Direct3D.ClearFlags.Target | Direct3D.ClearFlags.ZBuffer,
                          self.Background, 1, 0)
        self.Device.BeginScene()
 
        for mesh in (x for x in self.Objects.Values if hasattr(x, "GetWorldMatrix")):
            if callable(mesh.GetWorldMatrix):
                try:
                    self.Device.Transform.World = mesh.GetWorldMatrix()
 
                    materials = mesh.Materials
                    textures = mesh.Textures
 
                    for i in range(len(materials)):
                        self.Device.Material = materials[i]
 
                        if i < len(textures):
                            self.Device.SetTexture(0, textures[i])
                        else:
                            self.Device.SetTexture(0, None)
 
                        mesh.Mesh.DrawSubset(i)
 
                except Exception, e:
                    # We will only handle this if the mesh has a Name attribute (all meshes are supposed to
                    # have a Name, but it's possible we have been given a bogus object keyed to something
                    # other than its name).  This keeps us from infinitely poping up error messages if we
                    # actually cannot find the offending object to remove.
                    if hasattr(mesh, "Name"):
                        name = str(mesh.Name)
 
                        if name in self.Objects:
                            HandleException("An exception occurred while trying to render object " + name + ".", e)
                            del self.Objects[name]
 
        self.Device.EndScene()
        self.Device.Present()
 
    def Go(self, window):
        self.__FireEvent("OnSceneCreate", self)
        self.__FireEvent("OnSceneBegin", self)
        lastTick = System.Environment.TickCount
 
        while window.Created:
            currTick = System.Environment.TickCount
            self.__FireEvent("OnFrame", (currTick-lastTick)/1000.0)
            lastTick = currTick
 
            self.Render()
            Forms.Application.DoEvents()
 
    def AddEventListener(self, event, function):
        if not callable(function):
            raise TypeError, "Listeners must be callable."
        self.Listeners[event].append(function)
 
 
    def RemoveListenerByInstance(self, event, function):
        try:
            self.Listeners[event].Remove(function)
 
        except:
            # We will ignore any errors here...  This could potentially throw
            # exceptions if we ask to remove the function after the function
            # has already been removed.
            pass
 
 
    def RemoveListenerByType(self, event, type):
        for f in self.Listeners[event]:
            if hasattr(f, 'im_class'):
                if f.im_class == type:
                    self.Listeners[event].Remove(f)
 
    def RemoveListener(self, obj):
        """Takes in either a class or an instance of a class.  If obj is a class
        then this removes all instances of that class from all events.  If the
        obj parameter is an instance then it removes only that instance from all
        events it is registered for.  If obj is a sequence type then RemoveListener
        will be recursively called with the contents of the list (do NOT pass
        recursive sequences to this function or you will end up with an infinite
        loop)."""
        if operator.isSequenceType(obj):
            for x in obj:
                self.RemoveListener(x)
        else:
            for key in self.Listeners.Keys:
                self.RemoveListenerByInstance(key, obj)
                key = str(key)
                if hasattr(obj, key):
                    if issubclass(type(obj), type) or type(obj) == ClassType:
                        self.RemoveListenerByType(key, obj)
                    else:
                        f = getattr(obj, key)
                        self.RemoveListenerByInstance(key, f)
 
 
    def AddListener(self, obj):
        """Registers an object for all listeners that it has functions for.  If
        obj is an instance of a class, this will register that instance.  If obj
        is a class, then this object will attempt to construct an instance of it
        (without any parameters to the constructor) and then register that
        instance (this throws an exception if the constructor required args).
        Finally if obj is a sequence type this function will recursively call
        itself with the contents of the sequence.  Do not pass in a recursive
        list to this method."""
        if operator.isSequenceType(obj):
            for x in obj:
                self.AddListener(x)
 
        else:
            if issubclass(type(obj), type) or type(obj) == ClassType:
                obj = obj()
 
            for key in self.Listeners.Keys:
                if hasattr(obj, str(key)):
                    function = getattr(obj, str(key))
                    self.AddEventListener(key, function)
 
    def __FireEvent(self, event, arg):
        listeners = self.Listeners[event]
        for f in listeners:
            remove = False
            try:
                remove = f(arg)
            except Exception, e:
                HandleException("An exception occurred while firing an event for " + GetFunctionName(f) + ":", e)
                remove = True
 
            if remove:
                listeners.Remove(f)
 
 
 
class Root(object):
    """A borg class which keeps track of the RenderWindow and SceneManager
    classes."""
    __State = {}
    def __init__(self):
        self.__dict__ = self.__State
 
    def Init(self):
        self.SceneManager = SceneManager()
        self.Window = RenderWindow(self.SceneManager)
        self.InputManager = InputManager()
        self.Window.InputManager = self.InputManager
 
        # Register to handle close message
        self.InputManager.AddListener(self)
        self.Initialized = True
 
    def Main(self, listeners = []):
        """Runs the application, registering the list of listeners given by the
        listeners parameter.  Listeners can be an instance of a Listener class,
        it could be a class object of a listener class (in which case an
        instance will be created by calling the constructor with no params),
        or it could be a list of either of those."""
        if not hasattr(self, "Initialized"):
            self.Init()
 
        if not self.SceneManager.InitGraphics(self.Window.Handle):
            Forms.MessageBox.Show("Could not init Direct3D.")
 
        else:
            if listeners is not None:
                self.SceneManager.AddListener(listeners)
 
            self.Window.Show()
            self.SceneManager.Go(self.Window)
 
    def ThreadMain(self, listeners = []):
        "Starts main in a background thread."
        if not hasattr(self, "Initialized"):
            self.Init()
 
        args = (listeners,)
        thread.start_new_thread(self.Main, args)
 
    def OnKeyDown(self, key):
        if key == Forms.Keys.Escape:
            self.Window.Dispose()
            return True
 
if __name__ == '__main__':
    Forms.MessageBox.Show("The framework should not be run directly.\nRun one of the demos included with the tutorial instead.", "Error")