Normand Briere
2019-07-06 77633b188d01228383ced79c26b41ebb2989624c
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
/**
 * Make a donation http://sourceforge.net/donate/index.php?group_id=98797
 * Microcrowd.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Contact Josh DeFord jdeford@realvue.com
 */
 
package com.microcrowd.loader.java3d.max3ds;
 
import java.awt.Image;
import java.io.IOException;
import java.io.InputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.j3d.Behavior;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Light;
import javax.media.j3d.Texture;
import javax.media.j3d.TransformGroup;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;
import com.microcrowd.loader.java3d.max3ds.chunks.Chunk;
import com.microcrowd.loader.java3d.max3ds.data.KeyFramer;
import com.sun.j3d.loaders.SceneBase;
import com.sun.j3d.utils.image.TextureLoader;
 
/**
 * A singleton flyweight factory responsible for chopping the 
 * data up and sending it to the corresponding 
 * chunks(which are flyweights ala the flyweight pattern)
 * for processing.
 * This will sequentially read a 3ds file, load or
 * skip chunks and subchunks and initialize the data
 * for the chunks.  
 * <p>
 * Retrieved data may be stored as state in the ChunkChopper
 * via {@link #pushData} for use by other chunks.
 * <p> 
 * Features not supported; unknown chunks are skipped.
 */
public class ChunkChopper
{
    private Logger logger = Logger.getLogger(ChunkChopper.class.getName());
 
    private Loader3DS      loader;
    private BranchGroup    sceneGroup;
    private SceneBase      base;
    private HashMap        dataMap;
    private ByteBuffer     chunkBuffer;
    private Integer        chunkID;
 
    private TransformGroup currentGroup;
    private String         currentObjectName;
    private ChunkTester chunkTester = new ChunkTester();
    private Chunk mainChunk = new Chunk("MainChunk");
    private ChunkMap chunkMap = new ChunkMap(mainChunk);
 
    private KeyFramer keyFramer = new KeyFramer();
 
    /** This should be turned on by Loader3DS to view debugging information. */
    public static boolean debug;
 
    /** Current chunk for which debugging info is viewed if debug == true */
    public static Chunk debugChunk;
 
    /**
     * private singleton constructor.
     */
    public ChunkChopper(){}
 
    /**
     * This sequentially parses the chunks out of the input stream and
     * constructs the 3D entities represented within.
     * A Chunk is a little endian data structure consists of a 
     * 6 byte header followed by subchunks and or data.  
     * The first short int(little endian) represent the id 
     * of the chunk.  The next int represent the total 
     * length of the chunk(total of data, subchunks and chunk header).
     * <p> 
     * The first chunk is the main chunk (id=4D4D) and its length
     * is always the length of the file. It only contains sub chunks.
     * Other chunks may contain data, subchunks or both.  If the format
     * of a chunk is unknown skipped.
     * <p>
     * Subclasses of chunk will all automagically load the subchunks.  
     * It is the programmers responsibility to ensure that the data 
     * preceeding the subchunks is loaded or skipped as 
     * required and that something useful is done with the data.  If data from the
     * subchunks is needed in order to initialize components then that code should be
     * placed in {@link Chunk#initialize}.  Otherwise the data may be dealt with in
     * {@link Chunk#loadData}.  Also, if a chunk has data preceeding its subchunks
     * it communicates how many bytes long that data is by returning it from loadData.
     * <p>
     * This chopper reads a file in order from beginning to end
     * @param inputStream the stream with the data to be parsed.
     * @param loader the loader that will be configured from the data.
     * @param modelName name of the model file for display purposes.
     * @param modelSize size in bytes of the file to read.
     */
    public synchronized SceneBase loadSceneBase(InputStream inputStream, Loader3DS loader, int modelSize)
    {
        this.loader = loader;
        this.sceneGroup = new BranchGroup();
        this.base = new SceneBase();
        this.dataMap = new HashMap();
        base.setSceneGroup(sceneGroup);
 
        //FileChannel channel = null; 
        ReadableByteChannel channel = null; 
        try {
            channel = Channels.newChannel(inputStream);
            chunkBuffer = getByteBuffer(channel, modelSize);
            //chunkBuffer = getDirectByteBuffer(channel, modelSize);
 
            int mainChunkID     = chunkBuffer.getShort();
            long mainChunkLength = chunkBuffer.getInt();
 
            long begin = System.currentTimeMillis();     
            logger.finest("\n\n\n STARTING SUBCUNKS " + (mainChunkLength - 61));
            try {
                loadSubChunks(mainChunk, 0);
            }
            catch(CannotChopException e){
                
            }
            logger.finest("FINISHED WITH THE SUBCHUNKS");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally
        {
            try {
                if(channel != null) {
                    channel.close();
                }
            } catch (Exception e){
                //Just closing file.. don't care.
            }
        }
        return base;
    }
 
    /**
     * Allocates and loads a byte buffer from the channel
     * @param channel the file channel to load the data from
     * @return a direct byte buffer containing all the data of the channel at position 0 
     */
    public ByteBuffer getByteBuffer(ReadableByteChannel channel, int channelSize) throws IOException
    {
        ByteBuffer chunkBuffer = ByteBuffer.allocate(channelSize);
        chunkBuffer.order(ByteOrder.LITTLE_ENDIAN);
        channel.read(chunkBuffer);
        chunkBuffer.position(0);
        return chunkBuffer;
    }
 
 
    /**
     * The base class Chunk takes care of loading subchunks for
     * all chunks types.  It occurs as follows:
     * <ol>
     *     <li>The chunk id (short) is read
     *     <li>The chunk length(int) is read
     *     <li>A subchunk is looked up from the map of publish 
     *         subchunk types of the current chunk.
     *     <li>If it isn't found during the lookup it is skipped.
     *     <li>Otherwise it is requested to {@link #pushData} 
     *     <li>The return value, if there is one, is used to determine 
     *         where its next subchunk is. A return value of 0 signifies
     *         that the next subchunk is nigh.
     *     <li>The chunk's subchunks are then loaded.
     *     <li>The chunks initialize method is called.
     * </ol>
     */
    protected void loadSubChunks(Chunk parentChunk, int level) throws CannotChopException
    {
        level++;
        while(chunkBuffer.hasRemaining())//hasRemaining() evaluates limit - position.
        {
            chunkID         = new Integer(chunkBuffer.getShort());
            Chunk chunk     = parentChunk.getSubChunk(chunkID);
 
            int currentChunkLength     = chunkBuffer.getInt() - 6;  //length includes this 6 byte header.
            int finishedPosition       = chunkBuffer.position() + currentChunkLength;
            int previousLimit          = chunkBuffer.limit();
            chunkBuffer.limit(chunkBuffer.position() + currentChunkLength);
 
            if(debug) {
                debug(parentChunk, level, chunkID, currentChunkLength, chunkBuffer.position(), chunkBuffer.limit());
            }
            if(chunk != null && currentChunkLength != 0) {
                try {
                    chunk.loadData(this);
                }
                catch(BufferUnderflowException e){
                    chunkBuffer.position(finishedPosition);
                    chunkBuffer.limit(previousLimit);
                    throw new CannotChopException(" tried to read too much data from the buffer. Trying to recover.", e);
                }
                try {
                    if(chunkBuffer.hasRemaining()) {
                        loadSubChunks(chunk, level);
                    }
                    chunk.initialize(this);
                }
                catch(CannotChopException e){
                    logger.log(Level.SEVERE, chunk.toString() + "Trying to continue");
                }
            } 
 
            chunkBuffer.position(finishedPosition);
            chunkBuffer.limit(previousLimit);
        }
    }
 
    /**
     * Gets the key framer chunk
     * These should be their own objects instead of chunks.
     */
    public KeyFramer getKeyFramer()
    {
        return keyFramer;
    }
 
    /**
     * Adds a group to the choppers scene group
     * and sets the current name and group.
     * @param the name of the object to add which
     * will also be the current name of the object
     * the chopper is working with.
     * @param group the current group that the chopper
     * will be adding things too.
     */
    public void addObject(String name, TransformGroup group)
    {
        sceneGroup.addChild(group);
        currentGroup = group;
        currentObjectName = name;
        base.addNamedObject(name, group);
    }
 
 
 
    /**
     * Gets the name of the current object
     * the chopper is working with.  The value returned
     * from this generally is either set by a NamedObjectChunk 
     * and is the name of the name of the last object added
     * or is the name set by setObjectName.
     * @return the name of the current object being 
     * constructed.
     */
    public String getObjectName()
    {
        return currentObjectName;
    }
 
 
    /**
     * Sets the name of the current object.
     * The name of the current object can also be set
     * with {@link #addObject}
     * @param name the name that the current object should be set to.
     */
    public void setObjectName(String name)
    {
        currentObjectName = name;
    }
 
    /**
     * Gets the group for the current object 
     * the chopper is working with.  The value returned
     * from this generally gets set by a NamedObjectChunk 
     * and is the name of the last object added.
     * @return the group for the current object being 
     * constructed.
     */
    public TransformGroup getGroup()
    {
        return currentGroup;
    }
 
    /**
     * Used to store data that may later be used
     * by another chunk.
     * @param key the look up key.
     * @param data the data to store.
     */
    public void pushData(Object key, Object data)
    {
        dataMap.put(key, data);
    }
 
    /**
     * Gets a datum that had been retrieved and stored
     * via {@link #pushData} earlier and removes it.
     * @param key the key used to store the datum earlier.
     */
    public Object popData(Object key)
    {
        Object retVal = dataMap.remove(key);
        return retVal;
    }
 
    /**
     * Sets a named object in the loader.
     * @param key the key name of the object
     * @param value the named Object.
     */
    public void setNamedObject(String key, Object value)
    {
        base.addNamedObject(key, value);
    }
 
    /**
     * Returns true if there have been lights loaded.
     * @return true if there are lights.
     */
    public boolean hasLights()
    {
        return (base.getLightNodes() != null && base.getLightNodes().length > 0);
    }
 
    /**
     * Adds a behavior to the scene base.
     * @param behavior the behavior to add to the scene base.
     */
    public void addBehaviorNode(Behavior behavior)
    {
        base.addBehaviorNode(behavior);
    }
 
 
    /**
     * Adds a light to the scene base.
     * @param light the light to add to the scene base.
     */
    public void addLightNode(Light light)
    {
        base.addLightNode(light);
    }
 
 
    /**
     * Adds a camera transform to the scene base.
     * @param viewGroup the transform group to add as a view.
     */
    public void addViewGroup(TransformGroup viewGroup)
    {
        base.addViewGroup(viewGroup);
    }
 
    /**
     * Sets a named Object in the loader.
     * @param key the key used as the name for which the object will be returned
     */
    public Object getNamedObject(String key)
    {
        if(key == null)
            return null;
        return base.getNamedObjects().get(key);
    }
 
    /**
     * Gets and cast the named object for the
     * key provided.  Its an error if its not
     * a transform group.
     */
    public TransformGroup getNamedTransformGroup(String key)
    {
        Object object = getNamedObject(key);
        if(object instanceof TransformGroup)
        {
            return (TransformGroup)object;
        }
        else if (object != null)
        {
            logger.log(Level.INFO, "Retrieving " + key + " which is a named object but not useable because "+
                               " its not a transform group. Its a " + object.getClass().getName());
        }
        return null;
    }
 
 
    /**
     * Gets a long from the chunk Buffer
     */
    public long getLong()
    {
        return chunkBuffer.getLong();
    }
 
    /**
     * Reads a short and returns it as a signed
     * int.
     */
    public int getShort()
    {
        return chunkBuffer.getShort();
    }
 
    /**
     * Reads a short and returns it as an unsigned
     * int.
     */
    public int getUnsignedShort()
    {
        return chunkBuffer.getShort()&0xFFFF;
    }
 
    /**
     * reads a float from the chunkBuffer.
     */
    public float getFloat()
    {
        return chunkBuffer.getFloat();
    }
 
    /**
     * Reads 3 floats x,z,y from the chunkbuffer.
     * Since 3ds has z as up and y as pointing in whereas
     * java3d has z as pointing forward and y as pointing up;
     * this returns new Vector3f(x,-z,y)
     *
     */
    public Vector3f getVector()
    {
        return new Vector3f(getPoint());
    }
    /**
     * Reads 3 floats x,z,y from the chunkbuffer.
     * Since 3ds has z as up and y as pointing in whereas
     * java3d has z as pointing forward and y as pointing up;
     * this returns new Point3f(x,-z,y)
     */
    public Point3f getPoint()
    {
        float x = chunkBuffer.getFloat();
        float z = -chunkBuffer.getFloat();
        float y = chunkBuffer.getFloat();
        return new Point3f(x,y,z);
    }
 
    /**
     * Reads an int and returns it 
     * @return the int read
     */
    public int getInt()
    {
        return chunkBuffer.getInt();
    }
 
    /**
     * Reads an int and returns it 
     * unsigned, any ints greater than MAX_INT
     * will break.
     */
    public int getUnsignedInt()
    {
        return chunkBuffer.getInt()&0xFFFFFFFF;
    }
 
    /**
     * Reads a byte, unsigns it, returns the corresponding int.
     * @return the unsigned int corresponding to the read byte.
     */
    public int getUnsignedByte()
    {
        return chunkBuffer.get()&0xFF;
    }
 
    /**
     * Reads a number of bytes corresponding to the
     * number of bytes left in the current chunk and returns an array
     * containing them.
     * @return an array containing all the bytes for the current chunk. 
     */
    public byte[] getChunkBytes()
    {
        byte[] retVal = new byte[chunkBuffer.limit() - chunkBuffer.position()];
        get(retVal);
        return retVal;
    }
 
    /**
     * Fills bytes with data from the chunk buffer.
     * @param bytes the array to fill with data.
     */
    public void get(byte[] bytes)
    {
        chunkBuffer.get(bytes);
    }
 
 
    /**
     * Sets the data map used to store values
     * that chunks may need to retrieve later.
     * @param dataMap the hashmap that will be used to store
     * and retrieve values for use by chunks.
     */
    public void setDataMap(HashMap dataMap)
    {
        this.dataMap = dataMap;
    }
 
 
    /**
     * This reads bytes until it gets 0x00 and returns
     * the corresponding string.
     */
    public String getString()
    {
        StringBuffer stringBuffer = new StringBuffer();
        char charIn = (char)chunkBuffer.get();
        while(charIn != 0x00) 
        {
            stringBuffer.append(charIn);
            charIn = (char)chunkBuffer.get();
        }
        return stringBuffer.toString();
    }
 
    /**
     * Gets the id of the current chunk.
     * @return id of the current chunk as read
     * from the chunkBuffer.  It will be a signed <code>short</code>.
     */
    public Integer getID()
    {
        return chunkID;
    }
 
    /**
     * Loads the image to server as a texture.
     * @param textureImageName name of the image that 
     * is going to be set to be the texture.
     */
    public Texture createTexture(String textureImageName)
    {
        Image image = loader.getTextureImage(textureImageName);
        if(image == null)
        {
            System.err.println("Cannot load texture image " + textureImageName +
                               ". Make sure it is in the directory with the model file. " +
                               "If its a bmp make sure JAI is installed.");
            return null;
        }
        try 
        {
            TextureLoader textureLoader = new TextureLoader(image, null);
            return textureLoader.getTexture(); 
        }
        catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }
 
    /**
     * prints some handy information... the chunk hierarchy.
     */
    protected void debug(Chunk parentChunk, int level, Integer chunkID, long chunkLength, int position, long limit)
    {
        try {
       for(int i=0; i<level; i++)
       {
           System.out.print("  ");
       }
       Object child = parentChunk.getSubChunk(chunkID);
       int id = ((short)chunkID.intValue()) & 0xFFFF;
       System.out.println(parentChunk + " is " +
                         (child==null?"skipping":"LOADING")+
                         ": [id=" + Integer.toHexString(id) + 
                         ", object= <" + parentChunk.getSubChunk(chunkID) +
                         ">, chunkLength=" + chunkLength + 
                         ", position=" + position + 
                         " limit=" + limit + "]");
        }
        catch(Exception e){
            //We're debugging.. its ok
            e.printStackTrace();
        }
    }
 
    /**
     * Prints an exception and exits.
     */
    private void exceptAndExit(Throwable exception)
    {
        logger.log(Level.SEVERE, "\nThe chunk for loadData method read too much or not enough data from the stream." +
                            " It needs be skipped or adjusted to read more or less data.");
        exception.printStackTrace();
        System.exit(3);
    }
 
    /**
     * Convert the integer to an unsigned number.
     * @param i the integer to convert.
     */
    private static String byteString(int i) 
    {
        final char[] digits = {
       '0' , '1' , '2' , '3' , '4' , '5' ,
       '6' , '7' , '8' , '9' , 'a' , 'b' ,
       'c' , 'd' , 'e' , 'f' };
 
        char[] buf = new char[2];
        buf[1] = digits[i & 0xF];
        i >>>= 4;
        buf[0] = digits[i & 0xF];
 
        return "0x" + new String(buf).toUpperCase();
    }
}