/usr/share/pyshared/collada/scene.py is in python-collada 0.4-2.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 | ####################################################################
# #
# THIS FILE IS PART OF THE pycollada LIBRARY SOURCE CODE. #
# USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS #
# GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE #
# IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. #
# #
# THE pycollada SOURCE CODE IS (C) COPYRIGHT 2011 #
# by Jeff Terrace and contributors #
# #
####################################################################
"""This module contains several classes related to the scene graph.
Supported scene nodes are:
* <node> which is loaded as a Node
* <instance_camera> which is loaded as a CameraNode
* <instance_light> which is loaded as a LightNode
* <instance_material> which is loaded as a MaterialNode
* <instance_geometry> which is loaded as a GeometryNode
* <instance_controller> which is loaded as a ControllerNode
* <scene> which is loaded as a Scene
"""
import copy
import numpy
from collada.common import DaeObject, E, tag
from collada.common import DaeError, DaeIncompleteError, DaeBrokenRefError, \
DaeMalformedError, DaeUnsupportedError
from collada.util import toUnitVec
from collada.xmlutil import etree as ElementTree
class DaeInstanceNotLoadedError(Exception):
"""Raised when an instance_node refers to a node that isn't loaded yet. Will always be caught"""
def __init__(self, msg):
super(DaeInstanceNotLoadedError,self).__init__()
self.msg = msg
class SceneNode(DaeObject):
"""Abstract base class for all nodes within a scene."""
def objects(self, tipo, matrix=None):
"""Iterate through all objects under this node that match `tipo`.
The objects will be bound and transformed via the scene transformations.
:param str tipo:
A string for the desired object type. This can be one of 'geometry',
'camera', 'light', or 'controller'.
:param numpy.matrix matrix:
An optional transformation matrix
:rtype: generator that yields the type specified
"""
pass
def makeRotationMatrix(x, y, z, angle):
"""Build and return a transform 4x4 matrix to rotate `angle` radians
around (`x`,`y`,`z`) axis."""
c = numpy.cos(angle)
s = numpy.sin(angle)
t = (1-c)
return numpy.array([[t*x*x+c, t*x*y - s*z, t*x*z + s*y, 0],
[t*x*y+s*z, t*y*y + c, t*y*z - s*x, 0],
[t*x*z - s*y, t*y*z + s*x, t*z*z + c, 0],
[0, 0, 0, 1]],
dtype=numpy.float32 )
class Transform(DaeObject):
"""Base class for all transformation types"""
def save(self):
pass
class TranslateTransform(Transform):
"""Contains a translation transformation as defined in the collada <translate> tag."""
def __init__(self, x, y, z, xmlnode=None):
"""Creates a translation transformation
:param float x:
x coordinate
:param float y:
y coordinate
:param float z:
z coordinate
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.x = x
"""x coordinate"""
self.y = y
"""y coordinate"""
self.z = z
"""z coordinate"""
self.matrix = numpy.identity(4, dtype=numpy.float32)
"""The resulting transformation matrix. This will be a numpy.array of size 4x4."""
self.matrix[:3,3] = [ x, y, z ]
self.xmlnode = xmlnode
"""ElementTree representation of the transform."""
if xmlnode is None:
self.xmlnode = E.translate(' '.join([str(x),str(y),str(z)]))
@staticmethod
def load(collada, node):
floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
if len(floats) != 3:
raise DaeMalformedError("Translate node requires three float values")
return TranslateTransform(floats[0], floats[1], floats[2], node)
def __str__(self):
return '<TranslateTransform (%s, %s, %s)>' % (self.x, self.y, self.z)
def __repr__(self):
return str(self)
class RotateTransform(Transform):
"""Contains a rotation transformation as defined in the collada <rotate> tag."""
def __init__(self, x, y, z, angle, xmlnode=None):
"""Creates a rotation transformation
:param float x:
x coordinate
:param float y:
y coordinate
:param float z:
z coordinate
:param float angle:
angle of rotation, in radians
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.x = x
"""x coordinate"""
self.y = y
"""y coordinate"""
self.z = z
"""z coordinate"""
self.angle = angle
"""angle of rotation, in radians"""
self.matrix = makeRotationMatrix(x, y, z, angle*numpy.pi/180.0)
"""The resulting transformation matrix. This will be a numpy.array of size 4x4."""
self.xmlnode = xmlnode
"""ElementTree representation of the transform."""
if xmlnode is None:
self.xmlnode = E.rotate(' '.join([str(x),str(y),str(z),str(angle)]))
@staticmethod
def load(collada, node):
floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
if len(floats) != 4:
raise DaeMalformedError("Rotate node requires four float values")
return RotateTransform(floats[0], floats[1], floats[2], floats[3], node)
def __str__(self):
return '<RotateTransform (%s, %s, %s) angle=%s>' % (self.x, self.y, self.z, self.angle)
def __repr__(self):
return str(self)
class ScaleTransform(Transform):
"""Contains a scale transformation as defined in the collada <scale> tag."""
def __init__(self, x, y, z, xmlnode=None):
"""Creates a scale transformation
:param float x:
x coordinate
:param float y:
y coordinate
:param float z:
z coordinate
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.x = x
"""x coordinate"""
self.y = y
"""y coordinate"""
self.z = z
"""z coordinate"""
self.matrix = numpy.identity(4, dtype=numpy.float32)
"""The resulting transformation matrix. This will be a numpy.array of size 4x4."""
self.matrix[0,0] = x
self.matrix[1,1] = y
self.matrix[2,2] = z
self.xmlnode = xmlnode
"""ElementTree representation of the transform."""
if xmlnode is None:
self.xmlnode = E.scale(' '.join([str(x),str(y),str(z)]))
@staticmethod
def load(collada, node):
floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
if len(floats) != 3:
raise DaeMalformedError("Scale node requires three float values")
return ScaleTransform(floats[0], floats[1], floats[2], node)
def __str__(self):
return '<ScaleTransform (%s, %s, %s)>' % (self.x, self.y, self.z)
def __repr__(self):
return str(self)
class MatrixTransform(Transform):
"""Contains a matrix transformation as defined in the collada <matrix> tag."""
def __init__(self, matrix, xmlnode=None):
"""Creates a matrix transformation
:param numpy.array matrix:
This should be an unshaped numpy array of floats of length 16
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.matrix = matrix
"""The resulting transformation matrix. This will be a numpy.array of size 4x4."""
if len(self.matrix) != 16: raise DaeMalformedError('Corrupted matrix transformation node')
self.matrix.shape = (4, 4)
self.xmlnode = xmlnode
"""ElementTree representation of the transform."""
if xmlnode is None:
self.xmlnode = E.matrix(' '.join(map(str, self.matrix.flat)))
@staticmethod
def load(collada, node):
floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
return MatrixTransform(floats, node)
def __str__(self):
return '<MatrixTransform>'
def __repr__(self):
return str(self)
class LookAtTransform(Transform):
"""Contains a transformation for aiming a camera as defined in the collada <lookat> tag."""
def __init__(self, eye, interest, upvector, xmlnode=None):
"""Creates a lookat transformation
:param numpy.array eye:
An unshaped numpy array of floats of length 3 containing the position of the eye
:param numpy.array interest:
An unshaped numpy array of floats of length 3 containing the point of interest
:param numpy.array upvector:
An unshaped numpy array of floats of length 3 containing the up-axis direction
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.eye = eye
"""A numpy array of length 3 containing the position of the eye"""
self.interest = interest
"""A numpy array of length 3 containing the point of interest"""
self.upvector = upvector
"""A numpy array of length 3 containing the up-axis direction"""
if len(eye) != 3 or len(interest) != 3 or len(upvector) != 3:
raise DaeMalformedError('Corrupted lookat transformation node')
self.matrix = numpy.identity(4, dtype=numpy.float32)
"""The resulting transformation matrix. This will be a numpy.array of size 4x4."""
front = toUnitVec(numpy.subtract(eye,interest))
side = numpy.multiply(-1, toUnitVec(numpy.cross(front, upvector)))
self.matrix[0,0:3] = side
self.matrix[1,0:3] = upvector
self.matrix[2,0:3] = front
self.matrix[3,0:3] = eye
self.xmlnode = xmlnode
"""ElementTree representation of the transform."""
if xmlnode is None:
self.xmlnode = E.lookat(' '.join(map(str,
numpy.concatenate((self.eye, self.interest, self.upvector)) )))
@staticmethod
def load(collada, node):
floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
if len(floats) != 9:
raise DaeMalformedError("Lookat node requires 9 float values")
return LookAtTransform(floats[0:3], floats[3:6], floats[6:9], node)
def __str__(self):
return '<LookAtTransform>'
def __repr__(self):
return str(self)
class Node(SceneNode):
"""Represents a node object, which is a point on the scene graph, as defined in the collada <node> tag.
Contains the list of transformations effecting the node as well as any children.
"""
def __init__(self, id, children=None, transforms=None, xmlnode=None):
"""Create a node in the scene graph.
:param str id:
A unique string identifier for the node
:param list children:
A list of child nodes of this node. This can contain any
object that inherits from :class:`collada.scene.SceneNode`
:param list transforms:
A list of transformations effecting the node. This can
contain any object that inherits from :class:`collada.scene.Transform`
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.id = id
"""The unique string identifier for the node"""
self.children = []
"""A list of child nodes of this node. This can contain any
object that inherits from :class:`collada.scene.SceneNode`"""
if children is not None:
self.children = children
self.transforms = []
if transforms is not None:
self.transforms = transforms
"""A list of transformations effecting the node. This can
contain any object that inherits from :class:`collada.scene.Transform`"""
self.matrix = numpy.identity(4, dtype=numpy.float32)
"""A numpy.array of size 4x4 containing a transformation matrix that
combines all the transformations in :attr:`transforms`. This will only
be updated after calling :meth:`save`."""
for t in self.transforms:
self.matrix = numpy.dot(self.matrix, t.matrix)
if xmlnode is not None:
self.xmlnode = xmlnode
"""ElementTree representation of the transform."""
else:
self.xmlnode = E.node(id=self.id, name=self.id)
for t in self.transforms:
self.xmlnode.append(t.xmlnode)
for c in self.children:
self.xmlnode.append(c.xmlnode)
def objects(self, tipo, matrix=None):
"""Iterate through all objects under this node that match `tipo`.
The objects will be bound and transformed via the scene transformations.
:param str tipo:
A string for the desired object type. This can be one of 'geometry',
'camera', 'light', or 'controller'.
:param numpy.matrix matrix:
An optional transformation matrix
:rtype: generator that yields the type specified
"""
if matrix != None: M = numpy.dot( matrix, self.matrix )
else: M = self.matrix
for node in self.children:
for obj in node.objects(tipo, M):
yield obj
def save(self):
"""Saves the geometry back to :attr:`xmlnode`. Also updates
:attr:`matrix` if :attr:`transforms` has been modified."""
self.matrix = numpy.identity(4, dtype=numpy.float32)
for t in self.transforms:
self.matrix = numpy.dot(self.matrix, t.matrix)
for child in self.children:
child.save()
if self.id is not None:
self.xmlnode.set('id', self.id)
self.xmlnode.set('name', self.id)
for t in self.transforms:
if t.xmlnode not in self.xmlnode:
self.xmlnode.append(t.xmlnode)
for c in self.children:
if c.xmlnode not in self.xmlnode:
self.xmlnode.append(c.xmlnode)
xmlnodes = [c.xmlnode for c in self.children]
xmlnodes.extend([t.xmlnode for t in self.transforms])
for n in self.xmlnode:
if n not in xmlnodes:
self.xmlnode.remove(n)
@staticmethod
def load( collada, node, localscope ):
id = node.get('id')
children = []
transforms = []
for subnode in node:
try:
n = loadNode(collada, subnode, localscope)
if isinstance(n, Transform):
transforms.append(n)
elif n is not None:
children.append(n)
except DaeError as ex:
collada.handleError(ex)
return Node(id, children, transforms, xmlnode=node)
def __str__(self):
return '<Node transforms=%d, children=%d>' % (len(self.transforms), len(self.children))
def __repr__(self):
return str(self)
class NodeNode(Node):
"""Represents a node being instantiated in a scene, as defined in the collada <instande_node> tag."""
def __init__(self, node, xmlnode=None):
"""Creates a node node
:param collada.scene.Node node:
A node to instantiate in the scene
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.node = node
"""An object of type :class:`collada.scene.Node` representing the node to bind in the scene"""
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the node node."""
else:
self.xmlnode = E.instance_node(url="#%s" % self.node.id)
def objects(self, tipo, matrix=None):
for obj in self.node.objects(tipo, matrix):
yield obj
id = property(lambda s: s.node.id)
children = property(lambda s: s.node.children)
matrix = property(lambda s: s.node.matrix)
@staticmethod
def load( collada, node, localscope ):
url = node.get('url')
if not url.startswith('#'):
raise DaeMalformedError('Invalid url in node instance %s' % url)
referred_node = localscope.get(url[1:])
if not referred_node:
referred_node = collada.nodes.get(url[1:])
if not referred_node:
raise DaeInstanceNotLoadedError('Node %s not found in library'%url)
return NodeNode(referred_node, xmlnode=node)
def save(self):
"""Saves the node node back to :attr:`xmlnode`"""
self.xmlnode.set('url', "#%s" % self.node.id)
def __str__(self):
return '<NodeNode node=%s>' % (self.node.id,)
def __repr__(self):
return str(self)
class GeometryNode(SceneNode):
"""Represents a geometry instance in a scene, as defined in the collada <instance_geometry> tag."""
def __init__(self, geometry, materials=None, xmlnode=None):
"""Creates a geometry node
:param collada.geometry.Geometry geometry:
A geometry to instantiate in the scene
:param list materials:
A list containing items of type :class:`collada.scene.MaterialNode`.
Each of these represents a material that the geometry should be
bound to.
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.geometry = geometry
"""An object of type :class:`collada.geometry.Geometry` representing the
geometry to bind in the scene"""
self.materials = []
"""A list containing items of type :class:`collada.scene.MaterialNode`.
Each of these represents a material that the geometry is bound to."""
if materials is not None:
self.materials = materials
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the geometry node."""
else:
self.xmlnode = E.instance_geometry(url="#%s" % self.geometry.id)
if len(self.materials) > 0:
self.xmlnode.append(E.bind_material(
E.technique_common(
*[mat.xmlnode for mat in self.materials]
)
))
def objects(self, tipo, matrix=None):
"""Yields a :class:`collada.geometry.BoundGeometry` if ``tipo=='geometry'``"""
if tipo == 'geometry':
if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
materialnodesbysymbol = {}
for mat in self.materials:
materialnodesbysymbol[mat.symbol] = mat
yield self.geometry.bind(matrix, materialnodesbysymbol)
@staticmethod
def load( collada, node ):
url = node.get('url')
if not url.startswith('#'): raise DaeMalformedError('Invalid url in geometry instance %s' % url)
geometry = collada.geometries.get(url[1:])
if not geometry: raise DaeBrokenRefError('Geometry %s not found in library'%url)
matnodes = node.findall('%s/%s/%s'%( tag('bind_material'), tag('technique_common'), tag('instance_material') ) )
materials = []
for matnode in matnodes:
materials.append( MaterialNode.load(collada, matnode) )
return GeometryNode( geometry, materials, xmlnode=node)
def save(self):
"""Saves the geometry node back to :attr:`xmlnode`"""
self.xmlnode.set('url', "#%s" % self.geometry.id)
for m in self.materials:
m.save()
matparent = self.xmlnode.find('%s/%s'%( tag('bind_material'), tag('technique_common') ) )
if matparent is None and len(self.materials)==0:
return
elif matparent is None:
matparent = E.technique_common()
self.xmlnode.append(E.bind_material(matparent))
elif len(self.materials) == 0 and matparent is not None:
bindnode = self.xmlnode.find('%s' % tag('bind_material'))
self.xmlnode.remove(bindnode)
return
for m in self.materials:
if m.xmlnode not in matparent:
matparent.append(m.xmlnode)
xmlnodes = [m.xmlnode for m in self.materials]
for n in matparent:
if n not in xmlnodes:
matparent.remove(n)
def __str__(self):
return '<GeometryNode geometry=%s>' % (self.geometry.id,)
def __repr__(self):
return str(self)
class ControllerNode(SceneNode):
"""Represents a controller instance in a scene, as defined in the collada <instance_controller> tag. **This class is highly
experimental. More support will be added in version 0.4.**"""
def __init__(self, controller, materials, xmlnode=None):
"""Creates a controller node
:param collada.controller.Controller controller:
A controller to instantiate in the scene
:param list materials:
A list containing items of type :class:`collada.scene.MaterialNode`.
Each of these represents a material that the controller should be
bound to.
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.controller = controller
""" An object of type :class:`collada.controller.Controller` representing
the controller being instantiated in the scene"""
self.materials = materials
"""A list containing items of type :class:`collada.scene.MaterialNode`.
Each of these represents a material that the controller is bound to."""
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the controller node."""
else:
self.xmlnode = ElementTree.Element( tag('instance_controller') )
bindnode = ElementTree.Element( tag('bind_material') )
technode = ElementTree.Element( tag('technique_common') )
bindnode.append( technode )
self.xmlnode.append( bindnode )
for mat in materials: technode.append( mat.xmlnode )
def objects(self, tipo, matrix=None):
"""Yields a :class:`collada.controller.BoundController` if ``tipo=='controller'``"""
if tipo == 'controller':
if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
materialnodesbysymbol = {}
for mat in self.materials:
materialnodesbysymbol[mat.symbol] = mat
yield self.controller.bind(matrix, materialnodesbysymbol)
@staticmethod
def load( collada, node ):
url = node.get('url')
if not url.startswith('#'): raise DaeMalformedError('Invalid url in controller instance %s' % url)
controller = collada.controllers.get(url[1:])
if not controller: raise DaeBrokenRefError('Controller %s not found in library'%url)
matnodes = node.findall('%s/%s/%s'%( tag('bind_material'), tag('technique_common'), tag('instance_material') ) )
materials = []
for matnode in matnodes:
materials.append( MaterialNode.load(collada, matnode) )
return ControllerNode( controller, materials, xmlnode=node)
def save(self):
"""Saves the controller node back to :attr:`xmlnode`"""
self.xmlnode.set('url', '#'+self.controller.id)
for mat in self.materials:
mat.save()
def __str__(self):
return '<ControllerNode controller=%s>' % (self.controller.id,)
def __repr__(self):
return str(self)
class MaterialNode(SceneNode):
"""Represents a material being instantiated in a scene, as defined in the collada <instance_material> tag."""
def __init__(self, symbol, target, inputs, xmlnode = None):
"""Creates a material node
:param str symbol:
The symbol within a geometry this material should be bound to
:param collada.material.Material target:
The material object being bound to
:param list inputs:
A list of tuples of the form ``(semantic, input_semantic, set)`` mapping
texcoords or other inputs to material input channels, e.g.
``('TEX0', 'TEXCOORD', '0')`` would map the effect parameter ``'TEX0'``
to the ``'TEXCOORD'`` semantic of the geometry, using texture coordinate
set ``0``.
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.symbol = symbol
"""The symbol within a geometry this material should be bound to"""
self.target = target
"""An object of type :class:`collada.material.Material` representing the material object being bound to"""
self.inputs = inputs
"""A list of tuples of the form ``(semantic, input_semantic, set)`` mapping
texcoords or other inputs to material input channels, e.g.
``('TEX0', 'TEXCOORD', '0')`` would map the effect parameter ``'TEX0'``
to the ``'TEXCOORD'`` semantic of the geometry, using texture coordinate
set ``0``."""
if xmlnode is not None:
self.xmlnode = xmlnode
"""ElementTree representation of the material node."""
else:
self.xmlnode = E.instance_material(
*[E.bind_vertex_input(semantic=sem, input_semantic=input_sem, input_set=set)
for sem, input_sem, set in self.inputs]
, **{'symbol': self.symbol, 'target':"#%s"%self.target.id} )
@staticmethod
def load(collada, node):
inputs = []
for inputnode in node.findall( tag('bind_vertex_input') ):
inputs.append( ( inputnode.get('semantic'), inputnode.get('input_semantic'), inputnode.get('input_set') ) )
targetid = node.get('target')
if not targetid.startswith('#'): raise DaeMalformedError('Incorrect target id in material '+targetid)
target = collada.materials.get(targetid[1:])
if not target: raise DaeBrokenRefError('Material %s not found'%targetid)
return MaterialNode(node.get('symbol'), target, inputs, xmlnode = node)
def objects(self):
pass
def save(self):
"""Saves the material node back to :attr:`xmlnode`"""
self.xmlnode.set('symbol', self.symbol)
self.xmlnode.set('target', "#%s"%self.target.id)
inputs_in = []
for i in self.xmlnode.findall( tag('bind_vertex_input') ):
input_tuple = ( i.get('semantic'), i.get('input_semantic'), i.get('input_set') )
if input_tuple not in self.inputs:
self.xmlnode.remove(i)
else:
inputs_in.append(input_tuple)
for i in self.inputs:
if i not in inputs_in:
self.xmlnode.append(E.bind_vertex_input(semantic=i[0], input_semantic=i[1], input_set=i[2]))
def __str__(self):
return '<MaterialNode symbol=%s targetid=%s>' % (self.symbol, self.target.id)
def __repr__(self):
return str(self)
class CameraNode(SceneNode):
"""Represents a camera being instantiated in a scene, as defined in the collada <instance_camera> tag."""
def __init__(self, camera, xmlnode=None):
"""Create a camera instance
:param collada.camera.Camera camera:
The camera being instantiated
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.camera = camera
"""An object of type :class:`collada.camera.Camera` representing the instantiated camera"""
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the camera node."""
else:
self.xmlnode = E.instance_camera(url="#%s"%camera.id)
def objects(self, tipo, matrix=None):
"""Yields a :class:`collada.camera.BoundCamera` if ``tipo=='camera'``"""
if tipo == 'camera':
if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
yield self.camera.bind(matrix)
@staticmethod
def load( collada, node ):
url = node.get('url')
if not url.startswith('#'): raise DaeMalformedError('Invalid url in camera instance %s' % url)
camera = collada.cameras.get(url[1:])
if not camera: raise DaeBrokenRefError('Camera %s not found in library'%url)
return CameraNode( camera, xmlnode=node)
def save(self):
"""Saves the camera node back to :attr:`xmlnode`"""
self.xmlnode.set('url', '#'+self.camera.id)
def __str__(self):
return '<CameraNode camera=%s>' % (self.camera.id,)
def __repr__(self):
return str(self)
class LightNode(SceneNode):
"""Represents a light being instantiated in a scene, as defined in the collada <instance_light> tag."""
def __init__(self, light, xmlnode=None):
"""Create a light instance
:param collada.light.Light light:
The light being instantiated
:param xmlnode:
When loaded, the xmlnode it comes from
"""
self.light = light
"""An object of type :class:`collada.light.Light` representing the instantiated light"""
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the light node."""
else:
self.xmlnode = E.instance_light(url="#%s"%light.id)
def objects(self, tipo, matrix=None):
"""Yields a :class:`collada.light.BoundLight` if ``tipo=='light'``"""
if tipo == 'light':
if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
yield self.light.bind(matrix)
@staticmethod
def load( collada, node ):
url = node.get('url')
if not url.startswith('#'): raise DaeMalformedError('Invalid url in light instance %s' % url)
light = collada.lights.get(url[1:])
if not light: raise DaeBrokenRefError('Light %s not found in library'%url)
return LightNode( light, xmlnode=node)
def save(self):
"""Saves the light node back to :attr:`xmlnode`"""
self.xmlnode.set('url', '#'+self.light.id)
def __str__(self): return '<LightNode light=%s>' % (self.light.id,)
def __repr__(self): return str(self)
class ExtraNode(SceneNode):
"""Represents extra information in a scene, as defined in a collada <extra> tag."""
def __init__(self, xmlnode):
"""Create an extra node which stores arbitrary xml
:param xmlnode:
Should be an ElementTree instance of tag type <extra>
"""
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the extra node."""
else:
self.xmlnode = E.extra()
def objects(self, tipo, matrix=None):
if tipo == 'extra':
for e in self.xmlnode.findall(tag(tipo)):
yield e
@staticmethod
def load( collada, node ):
return ExtraNode(node)
def save(self):
pass
def loadNode( collada, node, localscope ):
"""Generic scene node loading from a xml `node` and a `collada` object.
Knowing the supported nodes, create the appropiate class for the given node
and return it.
"""
if node.tag == tag('node'): return Node.load(collada, node, localscope)
elif node.tag == tag('translate'): return TranslateTransform.load(collada, node)
elif node.tag == tag('rotate'): return RotateTransform.load(collada, node)
elif node.tag == tag('scale'): return ScaleTransform.load(collada, node)
elif node.tag == tag('matrix'): return MatrixTransform.load(collada, node)
elif node.tag == tag('lookat'): return LookAtTransform.load(collada, node)
elif node.tag == tag('instance_geometry'): return GeometryNode.load(collada, node)
elif node.tag == tag('instance_camera'): return CameraNode.load(collada, node)
elif node.tag == tag('instance_light'): return LightNode.load(collada, node)
elif node.tag == tag('instance_controller'): return ControllerNode.load(collada, node)
elif node.tag == tag('instance_node'): return NodeNode.load(collada, node, localscope)
elif node.tag == tag('extra'):
return ExtraNode.load(collada, node)
elif node.tag == tag('asset'):
return None
else: raise DaeUnsupportedError('Unknown scene node %s' % str(node.tag))
class Scene(DaeObject):
"""The root object for a scene, as defined in a collada <scene> tag"""
def __init__(self, id, nodes, xmlnode=None, collada=None):
"""Create a scene
:param str id:
A unique string identifier for the scene
:param list nodes:
A list of type :class:`collada.scene.Node` representing the nodes in the scene
:param xmlnode:
When loaded, the xmlnode it comes from
:param collada:
The collada instance this is part of
"""
self.id = id
"""The unique string identifier for the scene"""
self.nodes = nodes
"""A list of type :class:`collada.scene.Node` representing the nodes in the scene"""
self.collada = collada
"""The collada instance this is part of"""
if xmlnode != None:
self.xmlnode = xmlnode
"""ElementTree representation of the scene node."""
else:
self.xmlnode = E.visual_scene(id=self.id)
for node in nodes:
self.xmlnode.append( node.xmlnode )
def objects(self, tipo):
"""Iterate through all objects in the scene that match `tipo`.
The objects will be bound and transformed via the scene transformations.
:param str tipo:
A string for the desired object type. This can be one of 'geometry',
'camera', 'light', or 'controller'.
:rtype: generator that yields the type specified
"""
matrix = None
for node in self.nodes:
for obj in node.objects(tipo, matrix): yield obj
@staticmethod
def load( collada, node ):
id = node.get('id')
nodes = []
tried_loading = []
succeeded = False
localscope = {}
for nodenode in node.findall(tag('node')):
try:
N = loadNode(collada, nodenode, localscope)
except DaeInstanceNotLoadedError as ex:
tried_loading.append((nodenode, ex))
except DaeError as ex:
collada.handleError(ex)
else:
if N is not None:
nodes.append( N )
if N.id and N.id not in localscope:
localscope[N.id] = N
succeeded = True
while len(tried_loading) > 0 and succeeded:
succeeded = False
next_tried = []
for nodenode, ex in tried_loading:
try:
N = loadNode(collada, nodenode, localscope)
except DaeInstanceNotLoadedError as ex:
next_tried.append((nodenode, ex))
except DaeError as ex:
collada.handleError(ex)
else:
if N is not None:
nodes.append( N )
succeeded = True
tried_loading = next_tried
if len(tried_loading) > 0:
for nodenode, ex in tried_loading:
raise DaeBrokenRefError(ex.msg)
return Scene(id, nodes, xmlnode=node, collada=collada)
def save(self):
"""Saves the scene back to :attr:`xmlnode`"""
self.xmlnode.set('id', self.id)
for node in self.nodes:
node.save()
if node.xmlnode not in self.xmlnode:
self.xmlnode.append(node.xmlnode)
xmlnodes = [n.xmlnode for n in self.nodes]
for node in self.xmlnode:
if node not in xmlnodes:
self.xmlnode.remove(node)
def __str__(self):
return '<Scene id=%s nodes=%d>' % (self.id, len(self.nodes))
def __repr__(self):
return str(self)
|