This file is indexed.

/usr/share/pyshared/collada/material.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
####################################################################
#                                                                  #
# 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                                 #
#                                                                  #
####################################################################

"""Module for material, effect and image loading

This module contains all the functionality to load and manage:
- Images in the image library
- Surfaces and samplers2D in effects
- Effects (that are now used as materials)

"""

import copy
import numpy

from collada.common import DaeObject, E, tag
from collada.common import DaeIncompleteError, DaeBrokenRefError, \
        DaeMalformedError, DaeUnsupportedError
from collada.util import falmostEqual, StringIO
from collada.xmlutil import etree as ElementTree

try:
    import Image as pil
except:
    pil = None


class DaeMissingSampler2D(Exception):
    """Raised when a <texture> tag references a texture without a sampler."""
    pass


class CImage(DaeObject):
    """Class containing data coming from a <image> tag.

    Basically is just the path to the file, but we give an extended
    functionality if PIL is available. You can in that case get the
    image object or numpy arrays in both int and float format. We
    named it CImage to avoid confusion with PIL's Image class.

    """
    def __init__(self, id, path, collada = None, xmlnode = None):
        """Create an image object.

        :param str id:
          A unique string identifier for the image
        :param str path:
          Path relative to the collada document where the image is located
        :param collada.Collada collada:
          The collada object this image belongs to
        :param xmlnode:
          If loaded from xml, the node this data comes from

        """
        self.id = id
        """The unique string identifier for the image"""
        self.path = path
        """Path relative to the collada document where the image is located"""

        self.collada = collada
        self._data = None
        self._pilimage = None
        self._uintarray = None
        self._floatarray = None
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the image."""
        else:
            self.xmlnode = E.image(
                E.init_from(path)
            , id=self.id, name=self.id)

    def getData(self):
        if self._data is None:
            try: self._data = self.collada.getFileData( self.path )
            except DaeBrokenRefError as ex:
                self._data = ''
                self.collada.handleError(ex)
        return self._data

    def getImage(self):
        if pil is None or self._pilimage == 'failed':
            return None
        if self._pilimage:
            return self._pilimage
        else:
            data = self.getData()
            if not data:
                self._pilimage = 'failed'
                return None
            try:
                self._pilimage = pil.open( StringIO(data) )
                self._pilimage.load()
            except IOError as ex:
                self._pilimage = 'failed'
                return None
            return self._pilimage

    def getUintArray(self):
        if self._uintarray == 'failed': return None
        if self._uintarray != None: return self._uintarray
        img = self.getImage()
        if not img:
            self._uintarray = 'failed'
            return None
        nchan = len(img.mode)
        self._uintarray = numpy.fromstring(img.tostring(), dtype=numpy.uint8)
        self._uintarray.shape = (img.size[1], img.size[0], nchan)
        return self._uintarray

    def getFloatArray(self):
        if self._floatarray == 'failed': return None
        if self._floatarray != None: return self._floatarray
        array = self.getUintArray()
        if array is None:
            self._floatarray = 'failed'
            return None
        self._floatarray = numpy.asarray( array, dtype=numpy.float32)
        self._floatarray *= 1.0/255.0
        return self._floatarray

    def setData(self, data):
        self._data = data
        self._floatarray = None
        self._uintarray = None
        self._pilimage = None

    data = property( getData, setData )
    """Raw binary image file data if the file is readable. If `aux_file_loader` was passed to
    :func:`collada.Collada.__init__`, this function will be called to retrieve the data.
    Otherwise, if the file came from the local disk, the path will be interpreted from
    the local file system. If the file was a zip archive, the archive will be searched."""
    pilimage = property( getImage )
    """PIL Image object if PIL is available and the file is readable."""
    uintarray = property( getUintArray )
    """Numpy array (height, width, nchannels) in integer format."""
    floatarray = property( getFloatArray )
    """Numpy float array (height, width, nchannels) with the image data normalized to 1.0."""

    @staticmethod
    def load( collada, localspace, node ):
        id = node.get('id')
        initnode = node.find( tag('init_from') )
        if initnode is None: raise DaeIncompleteError('Image has no file path')
        path = initnode.text
        return CImage(id, path, collada, xmlnode = node)

    def save(self):
        """Saves the image back to :attr:`xmlnode`. Only the :attr:`id` attribute is saved.
        The image itself will have to be saved to its original source to make modifications."""
        self.xmlnode.set('id', self.id)
        self.xmlnode.set('name', self.id)
        initnode = self.xmlnode.find( tag('init_from') )
        initnode.text = self.path

    def __str__(self):
        return '<CImage id=%s path=%s>' % (self.id, self.path)

    def __repr__(self):
        return str(self)


class Surface(DaeObject):
    """Class containing data coming from a <surface> tag.

    Collada materials use this to access to the <image> tag.
    The only extra information we store right now is the
    image format. In theory, this enables many more features
    according to the collada spec, but no one seems to actually
    use them in the wild, so for now, it's unimplemented.

    """

    def __init__(self, id, img, format=None, xmlnode=None):
        """Creates a surface.

        :param str id:
          A string identifier for the surface within the local scope of the material
        :param collada.material.CImage img:
          The image object
        :param str format:
          The format of the image
        :param xmlnode:
          If loaded from xml, the xml node

        """
        self.id = id
        """The string identifier for the surface within the local scope of the material"""
        self.image = img
        """:class:`collada.material.CImage` object from the image library."""
        self.format = format if format is not None else "A8R8G8B8"
        """Format string."""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the surface."""
        else:
            self.xmlnode = E.newparam(
                E.surface(
                    E.init_from(self.image.id),
                    E.format(self.format)
                , type="2D")
            , sid=self.id)

    @staticmethod
    def load( collada, localscope, node ):
        surfacenode = node.find( tag('surface') )
        if surfacenode is None: raise DaeIncompleteError('No surface found in newparam')
        if surfacenode.get('type') != '2D': raise DaeMalformedError('Hard to imagine a non-2D surface, isn\'t it?')
        initnode = surfacenode.find( tag('init_from') )
        if initnode is None: raise DaeIncompleteError('No init image found in surface')
        formatnode = surfacenode.find( tag('format') )
        if formatnode is None: format = None
        else: format = formatnode.text
        imgid = initnode.text
        id = node.get('sid')
        if imgid in localscope:
            img = localscope[imgid]
        else:
            img = collada.images.get(imgid)
        if img is None: raise DaeBrokenRefError("Missing image '%s' in surface '%s'" % (imgid, id))
        return Surface(id, img, format, xmlnode=node)

    def save(self):
        """Saves the surface data back to :attr:`xmlnode`"""
        surfacenode = self.xmlnode.find( tag('surface') )
        initnode = surfacenode.find( tag('init_from') )
        if self.format:
            formatnode = surfacenode.find( tag('format') )
            if formatnode is None:
                surfacenode.append(E.format(self.format))
            else:
                formatnode.text = self.format
        initnode.text = self.image.id
        self.xmlnode.set('sid', self.id)

    def __str__(self):
        return '<Surface id=%s>' % (self.id,)

    def __repr__(self):
        return str(self)


class Sampler2D(DaeObject):
    """Class containing data coming from <sampler2D> tag in material.

    Collada uses the <sampler2D> tag to map to a <surface>. The only
    information we store about the sampler right now is minfilter and
    magfilter. Theoretically, the collada spec has many more parameters
    here, but no one seems to be using them in the wild, so they are
    currently unimplemented.

    """
    def __init__(self, id, surface, minfilter=None, magfilter=None, xmlnode=None):
        """Create a Sampler2D object.

        :param str id:
          A string identifier for the sampler within the local scope of the material
        :param collada.material.Surface surface:
          Surface instance that this object samples from
        :param str minfilter:
          Minification filter string id, see collada spec for details
        :param str magfilter:
          Maximization filter string id, see collada spec for details
        :param xmlnode:
          If loaded from xml, the xml node

        """
        self.id = id
        """The string identifier for the sampler within the local scope of the material"""
        self.surface = surface
        """Surface instance that this object samples from"""
        self.minfilter = minfilter
        """Minification filter string id, see collada spec for details"""
        self.magfilter = magfilter
        """Maximization filter string id, see collada spec for details"""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the sampler."""
        else:
            sampler_node = E.sampler2D(E.source(self.surface.id))
            if minfilter:
                sampler_node.append(E.minfilter(self.minfilter))
            if magfilter:
                sampler_node.append(E.magfilter(self.magfilter))

            self.xmlnode = E.newparam(sampler_node, sid=self.id)

    @staticmethod
    def load( collada, localscope, node ):
        samplernode = node.find( tag('sampler2D') )
        if samplernode is None: raise DaeIncompleteError('No sampler found in newparam')
        sourcenode = samplernode.find( tag('source') )
        if sourcenode is None: raise DaeIncompleteError('No source found in sampler')
        minnode = samplernode.find( tag('minfilter') )
        if minnode is None: minfilter = None
        else: minfilter = minnode.text
        magnode = samplernode.find( tag('magfilter') )
        if magnode is None: magfilter = None
        else: magfilter = magnode.text

        surfaceid = sourcenode.text
        id = node.get('sid')
        surface = localscope.get(surfaceid)
        if surface is None or type(surface) != Surface: raise DaeBrokenRefError('Missing surface ' + surfaceid)
        return Sampler2D(id, surface, minfilter, magfilter, xmlnode=node)

    def save(self):
        """Saves the sampler data back to :attr:`xmlnode`"""
        samplernode = self.xmlnode.find( tag('sampler2D') )
        sourcenode = samplernode.find( tag('source') )
        if self.minfilter:
            minnode = samplernode.find( tag('minfilter') )
            minnode.text = self.minfilter
        if self.magfilter:
            maxnode = samplernode.find( tag('magfilter') )
            maxnode.text = self.magfilter
        sourcenode.text = self.surface.id
        self.xmlnode.set('sid', self.id)

    def __str__(self):
        return '<Sampler2D id=%s>' % (self.id,)

    def __repr__(self):
        return str(self)


class Map(DaeObject):
    """Class containing data coming from <texture> tag inside material.

    When a material defines its properties like `diffuse`, it can give you
    a color or a texture. In the latter, the texture is mapped with a
    sampler and a texture coordinate channel. If a material defined a texture
    for one of its properties, you'll find an object of this class in the
    corresponding attribute.

    """
    def __init__(self, sampler, texcoord, xmlnode=None):
        """Create a map instance to a sampler using a texcoord channel.

        :param collada.material.Sampler2D sampler:
          A sampler object to map
        :param str texcoord:
          Texture coordinate channel symbol to use
        :param xmlnode:
          If loaded from xml, the xml node

        """
        self.sampler = sampler
        """:class:`collada.material.Sampler2D` object to map"""
        self.texcoord = texcoord
        """Texture coordinate channel symbol to use"""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the map"""
        else:
            self.xmlnode = E.texture(texture=self.sampler.id, texcoord=self.texcoord)

    @staticmethod
    def load( collada, localscope, node ):
        samplerid = node.get('texture')
        texcoord = node.get('texcoord')
        sampler = localscope.get(samplerid)
        #Check for the sampler ID as the texture ID because some exporters suck
        if sampler is None:
            for s2d in localscope.itervalues():
                if type(s2d) is Sampler2D:
                    if s2d.surface.image.id == samplerid:
                        sampler = s2d
        if sampler is None or type(sampler) != Sampler2D:
            err = DaeMissingSampler2D('Missing sampler ' + samplerid + ' in node ' + node.tag)
            err.samplerid = samplerid
            raise err
        return Map(sampler, texcoord, xmlnode = node)

    def save(self):
        """Saves the map back to :attr:`xmlnode`"""
        self.xmlnode.set('texture', self.sampler.id)
        self.xmlnode.set('texcoord', self.texcoord)

    def __str__(self):
        return '<Map sampler=%s texcoord=%s>' % (self.sampler.id, self.texcoord)

    def __repr__(self):
        return str(self)

class OPAQUE_MODE:
    """The opaque mode of an effect."""
    A_ONE = 'A_ONE'
    """Takes the transparency information from the color's alpha channel, where the value 1.0 is opaque (default)."""
    RGB_ZERO = 'RGB_ZERO'
    """Takes the transparency information from the color's red, green, and blue
    channels, where the value 0.0 is opaque, with each channel modulated
    independently."""

class Effect(DaeObject):
    """Class containing data coming from an <effect> tag.
    """
    supported = [ 'emission', 'ambient', 'diffuse', 'specular',
                  'shininess', 'reflective', 'reflectivity',
                  'transparent', 'transparency', 'index_of_refraction' ]
    """Supported material properties list."""
    shaders = [ 'phong', 'lambert', 'blinn', 'constant']
    """Supported shader list."""

    def __init__(self, id, params, shadingtype, bumpmap = None, double_sided = False,
                       emission = (0.0, 0.0, 0.0, 1.0),
                       ambient = (0.0, 0.0, 0.0, 1.0),
                       diffuse = (0.0, 0.0, 0.0, 1.0),
                       specular = (0.0, 0.0, 0.0, 1.0),
                       shininess = 0.0,
                       reflective = (0.0, 0.0, 0.0, 1.0),
                       reflectivity = 0.0,
                       transparent = (0.0, 0.0, 0.0, 1.0),
                       transparency = None,
                       index_of_refraction = None,
                       opaque_mode = None,
                       xmlnode = None):
        """Create an effect instance out of properties.

        :param str id:
          A string identifier for the effect
        :param list params:
          A list containing elements of type :class:`collada.material.Sampler2D`
          and :class:`collada.material.Surface`
        :param str shadingtype:
          The type of shader to be used for this effect. Right now, we
          only supper the shaders listed in :attr:`shaders`
        :param `collada.material.Map` bumpmap:
          The bump map for this effect, or None if there isn't one
        :param bool double_sided:
          Whether or not the material should be rendered double sided
        :param emission:
          Either an RGBA-format tuple of four floats or an instance
          of :class:`collada.material.Map`
        :param ambient:
          Either an RGBA-format tuple of four floats or an instance
          of :class:`collada.material.Map`
        :param diffuse:
          Either an RGBA-format tuple of four floats or an instance
          of :class:`collada.material.Map`
        :param specular:
          Either an RGBA-format tuple of four floats or an instance
          of :class:`collada.material.Map`
        :param shininess:
          Either a single float or an instance of :class:`collada.material.Map`
        :param reflective:
          Either an RGBA-format tuple of four floats or an instance
          of :class:`collada.material.Map`
        :param reflectivity:
          Either a single float or an instance of :class:`collada.material.Map`
        :param tuple transparent:
          Either an RGBA-format tuple of four floats or an instance
          of :class:`collada.material.Map`
        :param transparency:
          Either a single float or an instance of :class:`collada.material.Map`
        :param float index_of_refraction:
          A single float indicating the index of refraction for perfectly
          refracted light
        :param `collada.material.OPAQUE_MODE` opaque_mode:
          The opaque mode for the effect. If not specified, defaults to A_ONE.
        :param xmlnode:
          If loaded from xml, the xml node

        """
        self.id = id
        """The string identifier for the effect"""
        self.params = params
        """A list containing elements of type :class:`collada.material.Sampler2D`
          and :class:`collada.material.Surface`"""
        self.shadingtype = shadingtype
        """String with the type of the shading."""
        self.bumpmap = bumpmap
        """Either the bump map of the effect of type :class:`collada.material.Map`
        or None if there is none."""
        self.double_sided = double_sided
        """A boolean indicating whether or not the material should be rendered double sided"""
        self.emission = emission
        """Either an RGB-format tuple of three floats or an instance
          of :class:`collada.material.Map`"""
        self.ambient = ambient
        """Either an RGB-format tuple of three floats or an instance
          of :class:`collada.material.Map`"""
        self.diffuse = diffuse
        """Either an RGB-format tuple of three floats or an instance
          of :class:`collada.material.Map`"""
        self.specular = specular
        """Either an RGB-format tuple of three floats or an instance
          of :class:`collada.material.Map`"""
        self.shininess = shininess
        """Either a single float or an instance of :class:`collada.material.Map`"""
        self.reflective = reflective
        """Either an RGB-format tuple of three floats or an instance
          of :class:`collada.material.Map`"""
        self.reflectivity = reflectivity
        """Either a single float or an instance of :class:`collada.material.Map`"""
        self.transparent = transparent
        """Either an RGB-format tuple of three floats or an instance
          of :class:`collada.material.Map`"""
        self.transparency = transparency
        """Either a single float or an instance of :class:`collada.material.Map`"""
        self.index_of_refraction = index_of_refraction
        """A single float indicating the index of refraction for perfectly
          refracted light"""
        self.opaque_mode = OPAQUE_MODE.A_ONE if opaque_mode is None else opaque_mode
        """The opaque mode for the effect. An instance of :class:`collada.material.OPAQUE_MODE`."""

        if self.transparency is None:
            if self.opaque_mode == OPAQUE_MODE.A_ONE:
                self.transparency = 1.0
            else:
                self.transparency = 0.0

        self._fixColorValues()

        if xmlnode is not None:
            self.xmlnode = xmlnode
            """ElementTree representation of the effect"""
        else:
            shadnode = E(self.shadingtype)

            for prop in self.supported:
                value = getattr(self, prop)
                if value is None: continue
                propnode = E(prop)
                if prop == 'transparent' and self.opaque_mode == OPAQUE_MODE.RGB_ZERO:
                    propnode.set('opaque', OPAQUE_MODE.RGB_ZERO)
                shadnode.append( propnode )
                if type(value) is Map:
                    propnode.append(value.xmlnode)
                elif type(value) is float:
                    propnode.append(E.float(str(value)))
                else:
                    propnode.append(E.color(' '.join(map(str, value) )))

            effect_nodes = [param.xmlnode for param in self.params]
            effect_nodes.append(E.technique(shadnode, sid='common'))
            self.xmlnode = E.effect(
                E.profile_COMMON(*effect_nodes)
            , id=self.id, name=self.id)


    @staticmethod
    def load(collada, localscope, node):
        localscope = {} # we have our own scope, shadow it
        params = []
        id = node.get('id')
        profilenode = node.find( tag('profile_COMMON') )
        if profilenode is None:
            raise DaeUnsupportedError('Found effect with profile other than profile_COMMON')

        #<image> can be local to a material instead of global in <library_images>
        for imgnode in profilenode.findall( tag('image') ):
            local_image = CImage.load(collada, localscope, imgnode)
            localscope[local_image.id] = local_image

            global_image_id = local_image.id
            uniquenum = 2
            while global_image_id in collada.images:
                global_image_id = local_image.id + "-" + uniquenum
                uniquenum += 1
            collada.images.append(local_image)

        for paramnode in profilenode.findall( tag('newparam') ):
            if paramnode.find( tag('surface') ) is not None:
                param = Surface.load(collada, localscope, paramnode)
                params.append(param)
                localscope[param.id] = param
            elif paramnode.find( tag('sampler2D') ) is not None:
                param = Sampler2D.load(collada, localscope, paramnode)
                params.append(param)
                localscope[param.id] = param
            else:
                floatnode = paramnode.find( tag('float') )
                if floatnode is None: floatnode = paramnode.find( tag('float2') )
                if floatnode is None: floatnode = paramnode.find( tag('float3') )
                if floatnode is None: floatnode = paramnode.find( tag('float4') )
                paramid = paramnode.get('sid')
                if floatnode is not None and paramid is not None and len(paramid) > 0 and floatnode.text is not None:
                    localscope[paramid] = [float(v) for v in floatnode.text.split()]
        tecnode = profilenode.find( tag('technique') )
        shadnode = None
        for shad in Effect.shaders:
            shadnode = tecnode.find(tag(shad))
            shadingtype = shad
            if not shadnode is None:
                break
        if shadnode is None: raise DaeIncompleteError('No material properties found in effect')
        props = {}
        for key in Effect.supported:
            pnode = shadnode.find( tag(key) )
            if pnode is None: props[key] = None
            else:
                try: props[key] = Effect._loadShadingParam(collada, localscope, pnode)
                except DaeMissingSampler2D as ex:
                    if ex.samplerid in collada.images:
                        #Whoever exported this collada file didn't include the proper references so we will create them
                        surf = Surface(ex.samplerid + '-surface', collada.images[ex.samplerid], 'A8R8G8B8')
                        sampler = Sampler2D(ex.samplerid, surf, None, None);
                        params.append(surf)
                        params.append(sampler)
                        localscope[surf.id] = surf
                        localscope[sampler.id] = sampler
                        try:
                            props[key] = Effect._loadShadingParam(
                                    collada, localscope, pnode)
                        except DaeUnsupportedError as ex:
                            props[key] = None
                            collada.handleError(ex)
                except DaeUnsupportedError as ex:
                    props[key] = None
                    collada.handleError(ex) # Give the chance to ignore error and load the rest
                
                if key == 'transparent' and key in props and props[key] is not None:
                    opaque_mode = pnode.get('opaque')
                    if opaque_mode is not None and opaque_mode == OPAQUE_MODE.RGB_ZERO:
                        props['opaque_mode'] = OPAQUE_MODE.RGB_ZERO
        props['xmlnode'] = node

        bumpnode = node.find('.//%s//%s' % (tag('extra'), tag('texture')))
        if bumpnode is not None:
            bumpmap =  Map.load(collada, localscope, bumpnode)
        else:
            bumpmap = None

        double_sided_node = node.find('.//%s//%s' % (tag('extra'), tag('double_sided')))
        double_sided = False
        if double_sided_node is not None and double_sided_node.text is not None:
            try:
                val = int(double_sided_node.text)
                if val == 1:
                    double_sided = True
            except ValueError:
                pass
        return Effect(id, params, shadingtype, bumpmap, double_sided, **props)

    @staticmethod
    def _loadShadingParam( collada, localscope, node ):
        """Load from the node a definition for a material property."""
        children = node.getchildren()
        if not children: raise DaeIncompleteError('Incorrect effect shading parameter '+node.tag)
        vnode = children[0]
        if vnode.tag == tag('color'):
            try:
                value = tuple([ float(v) for v in vnode.text.split() ])
            except ValueError as ex:
                raise DaeMalformedError('Corrupted color definition in effect '+id)
            except IndexError as ex:
                raise DaeMalformedError('Corrupted color definition in effect '+id)
        elif vnode.tag == tag('float'):
            try: value = float(vnode.text)
            except ValueError as ex:
                raise DaeMalformedError('Corrupted float definition in effect '+id)
        elif vnode.tag == tag('texture'):
            value = Map.load(collada, localscope, vnode)
        elif vnode.tag == tag('param'):
            refid = vnode.get('ref')
            if refid is not None and refid in localscope:
                value = localscope[refid]
            else:
                return None
        else:
            raise DaeUnsupportedError('Unknown shading param definition ' + \
                    vnode.tag)
        return value

    def _fixColorValues(self):
        for prop in self.supported:
            propval = getattr(self, prop)
            if isinstance(propval, tuple):
                if len(propval) < 4:
                    propval = list(propval)
                    while len(propval) < 3:
                        propval.append(0.0)
                    while len(propval) < 4:
                        propval.append(1.0)
                    setattr(self, prop, tuple(propval))

    def save(self):
        """Saves the effect back to :attr:`xmlnode`"""
        self.xmlnode.set('id', self.id)
        self.xmlnode.set('name', self.id)
        profilenode = self.xmlnode.find( tag('profile_COMMON') )
        tecnode = profilenode.find( tag('technique') )
        tecnode.set('sid', 'common')

        self._fixColorValues()

        for param in self.params:
            param.save()
            if param.xmlnode not in profilenode.getchildren():
                profilenode.insert(list(profilenode).index(tecnode),
                        param.xmlnode)

        deletenodes = []
        for oldparam in profilenode.findall( tag('newparam') ):
            if oldparam not in [param.xmlnode for param in self.params]:
                deletenodes.append(oldparam)
        for d in deletenodes:
            profilenode.remove(d)

        for shader in self.shaders:
            shadnode = tecnode.find(tag(shader))
            if shadnode is not None and shader != self.shadingtype:
                tecnode.remove(shadnode)

        def getPropNode(prop, value):
            propnode = E(prop)
            if prop == 'transparent' and self.opaque_mode == OPAQUE_MODE.RGB_ZERO:
                propnode.set('opaque', OPAQUE_MODE.RGB_ZERO)
            if type(value) is Map:
                propnode.append(copy.deepcopy(value.xmlnode))
            elif type(value) is float:
                propnode.append(E.float(str(value)))
            else:
                propnode.append(E.color(' '.join(map(str, value) )))
            return propnode

        shadnode = tecnode.find(tag(self.shadingtype))
        if shadnode is None:
            shadnode = E(self.shadingtype)
            for prop in self.supported:
                value = getattr(self, prop)
                if value is None: continue
                shadnode.append(getPropNode(prop, value))
            tecnode.append(shadnode)
        else:
            for prop in self.supported:
                value = getattr(self, prop)
                propnode = shadnode.find(tag(prop))
                if propnode is not None:
                    shadnode.remove(propnode)
                if value is not None:
                    shadnode.append(getPropNode(prop, value))

        double_sided_node = profilenode.find('.//%s//%s' % (tag('extra'), tag('double_sided')))
        if double_sided_node is None or double_sided_node.text is None:
            extranode = profilenode.find(tag('extra'))
            if extranode is None:
                extranode = E.extra()
                profilenode.append(extranode)

            teqnodes = extranode.findall(tag('technique'))
            goognode = None
            for teqnode in teqnodes:
                if teqnode.get('profile') == 'GOOGLEEARTH':
                    goognode = teqnode
                    break
            if goognode is None:
                goognode = E.technique(profile='GOOGLEEARTH')
                extranode.append(goognode)
            double_sided_node = goognode.find(tag('double_sided'))
            if double_sided_node is None:
                double_sided_node = E.double_sided()
                goognode.append(double_sided_node)

        double_sided_node.text = "1" if self.double_sided else "0"

    def __str__(self):
        return '<Effect id=%s type=%s>' % (self.id, self.shadingtype)

    def __repr__(self):
        return str(self)

    def almostEqual(self, other):
        """Checks if this effect is almost equal (within float precision)
        to the given effect.

        :param collada.material.Effect other:
          Effect to compare to

        :rtype: bool

        """
        if self.shadingtype != other.shadingtype:
            return False
        if self.double_sided != other.double_sided:
            return False
        for prop in self.supported:
            thisprop = getattr(self, prop)
            otherprop = getattr(other, prop)
            if type(thisprop) != type(otherprop):
                return False
            elif type(thisprop) is float:
                if not falmostEqual(thisprop, otherprop):
                    return False
            elif type(thisprop) is Map:
                if thisprop.sampler.surface.image.id != otherprop.sampler.surface.image.id or thisprop.texcoord != otherprop.texcoord:
                    return False
            elif type(thisprop) is tuple:
                if len(thisprop) != len(otherprop):
                    return False
                for valthis, valother in zip(thisprop, otherprop):
                    if not falmostEqual(valthis, valother):
                        return False
        return True


class Material(DaeObject):
    """Class containing data coming from a <material> tag.

    Right now, this just stores a reference to the effect
    which is instantiated in the material. The effect instance
    can have parameters, but this is rarely used in the wild,
    so it is not yet implemented.

    """

    def __init__(self, id, name, effect, xmlnode=None):
        """Creates a material.

        :param str id:
          A unique string identifier for the material
        :param str name:
          A name for the material
        :param collada.material.Effect effect:
          The effect instantiated in this material
        :param xmlnode:
          If loaded from xml, the xml node

        """

        self.id = id
        """The unique string identifier for the material"""
        self.name = name
        """The name for the material"""
        self.effect = effect
        """The :class:`collada.material.Effect` instantiated in this material"""

        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the surface."""
        else:
            self.xmlnode = E.material(
                E.instance_effect(url="#%s" % self.effect.id)
            , id=str(self.id), name=str(self.name))

    @staticmethod
    def load( collada, localscope, node ):
        matid = node.get('id')
        matname = node.get('name')

        effnode = node.find( tag('instance_effect'))
        if effnode is None: raise DaeIncompleteError('No effect inside material')
        effectid = effnode.get('url')

        if not effectid.startswith('#'):
            raise DaeMalformedError('Corrupted effect reference in material %s' % effectid)

        effect = collada.effects.get(effectid[1:])
        if not effect:
            raise DaeBrokenRefError('Effect not found: '+effectid)

        return Material(matid, matname, effect, xmlnode=node)

    def save(self):
        """Saves the material data back to :attr:`xmlnode`"""
        self.xmlnode.set('id', str(self.id))
        self.xmlnode.set('name', str(self.name))
        effnode = self.xmlnode.find( tag('instance_effect') )
        effnode.set('url', '#%s' % self.effect.id)

    def __str__(self):
        return '<Material id=%s effect=%s>' % (self.id, self.effect.id)

    def __repr__(self):
        return str(self)