public static Clip ReadClip(ChunkReader reader)
        {
            // index
            var index			= reader.ReadUInt32();

            // first subchunk header
            var type			= reader.ReadID<ClipType>();
            var subchunk_size	= reader.ReadUInt16();
            subchunk_size += (ushort)(subchunk_size & 1);

            Clip clip = null;
            using (var subChunkReader = reader.GetSubChunk(subchunk_size))
            {
                switch (type)
                {
                    case ClipType.ID_STIL:
                    {
                        var tmp_clip	= new ClipStill(index);
                        tmp_clip.Name = subChunkReader.ReadString();
                        clip = tmp_clip;
                        break;
                    }

                    case ClipType.ID_ISEQ:
                    {
                        var tmp_clip	= new ClipSequence(index);
                        tmp_clip.Digits = subChunkReader.ReadUInt8();
                        tmp_clip.Flags	= subChunkReader.ReadUInt8();
                        tmp_clip.Offset = subChunkReader.ReadSInt16();
                        subChunkReader.ReadUInt16();  // Legacy Cruft: Nothing to see here
                        tmp_clip.Start	= subChunkReader.ReadSInt16();
                        tmp_clip.End	= subChunkReader.ReadSInt16();
                        tmp_clip.Prefix = subChunkReader.ReadString();
                        tmp_clip.Suffix = subChunkReader.ReadString();
                        clip = tmp_clip;
                        break;
                    }

                    case ClipType.ID_ANIM:
                    {
                        var tmp_clip	= new ClipAnim(index);
                        tmp_clip.Name	= subChunkReader.ReadString();
                        tmp_clip.Server = subChunkReader.ReadString();
                        tmp_clip.Data	= subChunkReader.ReadBytes((uint)subChunkReader.BytesLeft);
                        clip = tmp_clip;
                        break;
                    }

                    case ClipType.ID_XREF:
                    {
                        var tmp_clip			= new ClipCloned(index);
                        tmp_clip.clip_reference_index	= subChunkReader.ReadUInt32();
                        tmp_clip.Name			= subChunkReader.ReadString();
                        clip = tmp_clip;
                        break;
                    }

                    case ClipType.ID_STCC:
                    {
                        var tmp_clip	= new ClipColorCycle(index);
                        tmp_clip.lo		= subChunkReader.ReadSInt16();
                        tmp_clip.hi		= subChunkReader.ReadSInt16();
                        tmp_clip.Name	= subChunkReader.ReadString();
                        clip = tmp_clip;
                        break;
                    }

                    default:
                        throw new Exception("Unknown Clip type"); // TODO: create proper exception class for this ...
                }
            }

            while (reader.BytesLeft > 0)
            {
                // process subchunks as they're encountered
                var id = reader.ReadID<ClipDataType>();
                subchunk_size = reader.ReadUInt16();
                subchunk_size += (ushort)(subchunk_size & 1);

                using (var subChunkReader = reader.GetSubChunk(subchunk_size))
                {
                    switch (id)
                    {
                        //case ClipDataType.ID_CLRS: // Color Space RGB   - CLRS { flags[U2], colorspace[U2], filename[FNAM0] }
                        //case ClipDataType.ID_CLRA: // Color Space Alpha - CLRA { flags[U2], colorspace[U2], filename[FNAM0] }
                        //case ClipDataType.ID_FILT: // Image Filtering   - FILT { flags[U2] }
                        //case ClipDataType.ID_DITH: // Image Dithering	  - DITH { flags[U2] }

                        // Contrast - CONT { contrast-delta[FP4], envelope[VX] }
                        case ClipDataType.ID_CONT: { clip.Contrast.value = subChunkReader.ReadSingle(); clip.Contrast.envelope_index = subChunkReader.ReadVariableLengthIndex(); break; }

                        // Brightness - BRIT { brightness-delta[FP4], envelope[VX] }
                        case ClipDataType.ID_BRIT: { clip.Brightness.value = subChunkReader.ReadSingle(); clip.Brightness.envelope_index = subChunkReader.ReadVariableLengthIndex(); break; }

                        // Saturation - SATR { saturation-delta[FP4], envelope[VX] }
                        case ClipDataType.ID_SATR: { clip.Saturation.value = subChunkReader.ReadSingle(); clip.Saturation.envelope_index = subChunkReader.ReadVariableLengthIndex(); break; }

                        // Hue - HUE { hue-rotation[FP4], envelope[VX] }
                        case ClipDataType.ID_HUE: { clip.Hue.value = subChunkReader.ReadSingle(); clip.Hue.envelope_index = subChunkReader.ReadVariableLengthIndex(); break; }

                        // Gamma Correction - GAMM { gamma[F4], envelope[VX] }
                        case ClipDataType.ID_GAMM: { clip.Gamma.value = subChunkReader.ReadSingle(); clip.Gamma.envelope_index = subChunkReader.ReadVariableLengthIndex(); break; }

                        // Negative - NEGA { enable[U2] }
                        case ClipDataType.ID_NEGA: { clip.Negative = subChunkReader.ReadUInt16() != 0; break; }

                        // Time - TIME { start-time[FP4], duration[FP4], frame-rate[FP4] }
                        case ClipDataType.ID_TIME:
                        {
                            clip.StartTime			= subChunkReader.ReadSingle();
                            clip.Duration			= subChunkReader.ReadSingle();
                            clip.FrameRate			= subChunkReader.ReadSingle();
                            break;
                        }

                        // Plug-in Image Filters - IFLT { server-name[S0], flags[U2], data[...] }
                        case ClipDataType.ID_IFLT:

                        // Plug-in Pixel Filters - PFLT { server-name[S0], flags[U2], data[...] }
                        case ClipDataType.ID_PFLT:
                        {
                            var filt = new LightwavePlugin();
                            filt.name	= subChunkReader.ReadString();
                            filt.flags	= subChunkReader.ReadUInt16();
                            filt.data	= subChunkReader.ReadBytes((uint)subChunkReader.BytesLeft);

                            if (id == ClipDataType.ID_IFLT)
                                clip.ImageFilters.Add(filt);
                            else
                                clip.PixelFilters.Add(filt);
                            break;
                        }

                        case ClipDataType.ID_FLAG: // not mentioned in documentation ...
                            var flags = subChunkReader.ReadUInt16(); // unknown what they mean ...
                            break;

                        default:
                            Console.WriteLine("Unknown clip type " + reader.GetIDString((uint)id));
                            break;
                    }
                }
            }

            return clip;
        }
        static LightwaveObject ReadObject2(ChunkReader reader)
        {
            // allocate an object and a default layer
            var newObject		= new LightwaveObject();
            var	currentLayer	= new Layer();
            newObject.Layers.Add(currentLayer);

            bool createdLayer	= false;
            uint pointOffset	= 0;
            uint polygonOffset	= 0;
            uint tagOffset		= 0;

            // process chunks as they're encountered
            while (reader.BytesLeft > 0)
            {
                var id = reader.ReadID<ChunkType>();
                var cksize	= reader.ReadUInt32();
                cksize += cksize & 1;

                using (var subchunkReader = reader.GetSubChunk(cksize))
                {
                    switch (id)
                    {
                        case ChunkType.ID_LAYR:
                        {
                            if (createdLayer)
                            {
                                currentLayer = new Layer();
                                newObject.Layers.Add(currentLayer);
                            }

                            createdLayer = true;
                            currentLayer.Index		= subchunkReader.ReadUInt16();
                            currentLayer.Flags		= subchunkReader.ReadUInt16();
                            currentLayer.pivot_x	= subchunkReader.ReadSingle();
                            currentLayer.pivot_y	= subchunkReader.ReadSingle();
                            currentLayer.pivot_z	= subchunkReader.ReadSingle();
                            currentLayer.Name		= subchunkReader.ReadString();
                            if (subchunkReader.BytesLeft > 2)
                                currentLayer.Parent = subchunkReader.ReadUInt16();
                            break;
                        }

                        case ChunkType.ID_PTAG:
                        {
                            ReadPolygonTags(subchunkReader, currentLayer.Polygons, polygonOffset, tagOffset);
                            break;
                        }

                        case ChunkType.ID_BBOX:
                        {
                            currentLayer.bbox_min_x = subchunkReader.ReadSingle();
                            currentLayer.bbox_min_y = subchunkReader.ReadSingle();
                            currentLayer.bbox_min_z = subchunkReader.ReadSingle();
                            currentLayer.bbox_max_x = subchunkReader.ReadSingle();
                            currentLayer.bbox_max_y = subchunkReader.ReadSingle();
                            currentLayer.bbox_max_z = subchunkReader.ReadSingle();
                            break;
                        }

                        case ChunkType.ID_PNTS:
                        {
                            pointOffset = (uint)currentLayer.Points.Count;
                            currentLayer.Points.AddRange(
                                        ReadPoints(subchunkReader)							// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_POLS:
                        {
                            polygonOffset = (uint)currentLayer.Polygons.Count;
                            currentLayer.Polygons.AddRange(
                                        ReadPolygons(subchunkReader, pointOffset)			// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_VMAP:
                        {
                            currentLayer.VertexMaps.Add(
                                        VertexMap.ReadVertexMap(subchunkReader, false)		// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_VMAD:
                        {
                            currentLayer.VertexMaps.Add(
                                        VertexMap.ReadVertexMap(subchunkReader, true)		// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_TAGS:
                        {
                            tagOffset = (uint)newObject.Tags.Count;
                            newObject.Tags.AddRange(
                                        LightwaveObject.ReadTags(subchunkReader)			// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_ENVL:
                        {
                            newObject.Envelopes.Add(
                                        Envelope.ReadEnvelope(subchunkReader)				// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_CLIP:
                        {
                            newObject.Clips.Add(
                                        Clip.ReadClip(subchunkReader)						// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_SURF:
                        {
                            newObject.Surfaces.Add(
                                        Surface.ReadSurface(subchunkReader)					// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_DESC: // Description Line - DESC { description-line[S0] }
                        case ChunkType.ID_TEXT: // Commentary Text - TEXT { comment[S0] }
                        case ChunkType.ID_ICON:	// Thumbnail Icon Image - ICON { encoding[U2], width[U2], data[U1] * }
                        case ChunkType.ID_VMPA: // Vertex Map Parameter - VMPA { UV subdivision type[I4], sketch color[I4] }
                            break;
                        default:
                            Console.WriteLine("Unknown chunk type " + reader.GetIDString((uint)id));
                            break;
                    }
                }
            }

            if (newObject.Tags.Count == 0)
                throw new Exception("No tags found for this layer");	// TODO: create a proper exception class

            uint layer_index = 0;
            foreach (var layer in newObject.Layers)
            {
                layer.CalculateBoundingBox();
                newObject.CalculatePolygonNormals(layer.Points, layer.Polygons);
                newObject.ResolvePointPolygons(layer.Points, layer.Polygons);
                newObject.ResolvePolygonSurfaces(layer.Polygons, newObject.Tags, newObject.Surfaces, layer_index);
                newObject.CalculateVertexNormals(layer.Points, layer.Polygons);
                newObject.ResolvePointVertexMaps(layer.Points, layer.VertexMaps);
                newObject.ResolvePolygonVertexMaps(layer.Polygons, layer.VertexMaps);
                layer_index++;
            }

            return newObject;
        }
        // Returns the contents of a LWOB
        static LightwaveObject ReadObject5(ChunkReader reader)
        {
            // allocate an object and a default layer
            var newObject		= new LightwaveObject();
            var currentLayer	= new Layer();
            newObject.Layers.Add(currentLayer);

            uint pointOffset	= 0;
            uint polygonOffset	= 0;
            uint tagOffset		= 0;

            // process chunks as they're encountered
            while (reader.BytesLeft > 0)
            {
                var id		= reader.ReadID<ChunkType>();
                var cksize	= reader.ReadUInt32();
                cksize += cksize & 1;

                using (var subchunkReader = reader.GetSubChunk(cksize))
                {
                    switch (id)
                    {
                        case ChunkType.ID_PNTS:
                        {
                            pointOffset = (uint)currentLayer.Points.Count;
                            currentLayer.Points.AddRange(
                                        ReadPoints(subchunkReader)							// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_POLS:
                        {
                            polygonOffset = (uint)currentLayer.Polygons.Count;
                            currentLayer.Polygons.AddRange(
                                        ReadPolygons5(subchunkReader, pointOffset)			// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_SRFS:
                        {
                            tagOffset = (uint)newObject.Tags.Count;
                            newObject.Tags.AddRange(
                                        LightwaveObject.ReadTags(subchunkReader)			// throws exception on failure
                                    );
                            break;
                        }

                        case ChunkType.ID_SURF:
                        {
                            newObject.Surfaces.Add(
                                        Surface.ReadSurface5(subchunkReader, newObject)		// throws exception on failure
                                    );
                            break;
                        }

                        default:
                            Console.WriteLine("Unknown chunk type " + reader.GetIDString((uint)id));
                            break;
                    }
                }
            }

            if (newObject.Tags.Count == 0)
                throw new Exception("No tags found for this layer");	// TODO: create a proper exception class

            uint layer_index = 0;
            foreach (var layer in newObject.Layers)
            {
                layer.CalculateBoundingBox();
                newObject.CalculatePolygonNormals(layer.Points, layer.Polygons);
                newObject.ResolvePointPolygons(layer.Points, layer.Polygons);
                newObject.ResolvePolygonSurfaces(layer.Polygons, newObject.Tags, newObject.Surfaces, layer_index);
                newObject.CalculateVertexNormals(layer.Points, layer.Polygons);
                layer_index++;
            }

            return newObject;
        }
        public static LightwaveObject LoadObject(string filename)
        {
            // open the file
            var contents = File.ReadAllBytes(filename);
            using (var reader = new ChunkReader(contents))
            {
                // read the first 12 bytes
                var id			= reader.ReadID<HeaderID>();
                var formsize	= reader.ReadUInt32();
                var type		= reader.ReadID<ObjectType>();

                if (id != HeaderID.ID_FORM) // is this a LW object?
                    return null;

                using (var formReader = reader.GetSubChunk(formsize))
                {
                    switch (type)
                    {
                        case ObjectType.ID_LWO2: return ReadObject2(formReader);
                        case ObjectType.ID_LWOB: return ReadObject5(formReader);
                        default:
                            Console.WriteLine("Unknown object type " + reader.GetIDString((uint)type));
                            return null;
                    }
                }
            }
        }