001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011package com.ardor3d.image.util.dds;
012
013import static com.ardor3d.image.util.dds.DdsUtils.flipDXT;
014import static com.ardor3d.image.util.dds.DdsUtils.getInt;
015import static com.ardor3d.image.util.dds.DdsUtils.isSet;
016import static com.ardor3d.image.util.dds.DdsUtils.shiftCount;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.nio.ByteBuffer;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.logging.Logger;
024
025import com.ardor3d.image.Image;
026import com.ardor3d.image.ImageDataFormat;
027import com.ardor3d.image.PixelDataType;
028import com.ardor3d.image.util.ImageLoader;
029import com.ardor3d.image.util.ImageUtils;
030import com.ardor3d.util.LittleEndianDataInput;
031import com.ardor3d.util.geom.BufferUtils;
032
033/**
034 * <p>
035 * <code>DdsLoader</code> is an image loader that reads in a DirectX DDS file.
036 * </p>
037 * Supports 2D images, volume images and cubemaps in the following formats:<br>
038 * Compressed:<br>
039 * <ul>
040 * <li>DXT1A</li>
041 * <li>DXT3</li>
042 * <li>DXT5</li>
043 * <li>LATC</li>
044 * </ul>
045 * Uncompressed:<br>
046 * <ul>
047 * <li>RGB</li>
048 * <li>RGBA</li>
049 * <li>Luminance</li>
050 * <li>LuminanceAlpha</li>
051 * <li>Alpha</li>
052 * </ul>
053 * Note that Cubemaps must have all 6 faces defined to load properly. FIXME: Needs a software inflater for compressed
054 * formats in cases where support is not present? Maybe JSquish?
055 */
056public class DdsLoader implements ImageLoader {
057    private static final Logger logger = Logger.getLogger(DdsLoader.class.getName());
058
059    @Override
060    public Image load(final InputStream is, final boolean flipVertically) throws IOException {
061        try (final LittleEndianDataInput in = new LittleEndianDataInput(is)) {
062
063            // Read and check magic word...
064            final int dwMagic = in.readInt();
065            if (dwMagic != getInt("DDS ")) {
066                throw new Error("Not a dds file.");
067            }
068            logger.finest("Reading DDS file.");
069
070            // Create our data store;
071            final DdsImageInfo info = new DdsImageInfo();
072
073            info.flipVertically = flipVertically;
074
075            // Read standard dds header
076            info.header = DdsHeader.read(in);
077
078            // if applicable, read DX10 header
079            info.headerDX10 = info.header.ddpf.dwFourCC == getInt("DX10") ? DdsHeaderDX10.read(in) : null;
080
081            // Create our new image
082            final Image image = new Image();
083            image.setWidth(info.header.dwWidth);
084            image.setHeight(info.header.dwHeight);
085
086            // update depth based on flags / header
087            updateDepth(image, info);
088
089            // add our format and image data.
090            populateImage(image, info, in);
091
092            // return the loaded image
093            return image;
094        }
095    }
096
097    private static final void updateDepth(final Image image, final DdsImageInfo info) {
098        if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP)) {
099            int depth = 0;
100            if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEX)) {
101                depth++;
102            }
103            if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEX)) {
104                depth++;
105            }
106            if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEY)) {
107                depth++;
108            }
109            if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEY)) {
110                depth++;
111            }
112            if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEZ)) {
113                depth++;
114            }
115            if (isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEZ)) {
116                depth++;
117            }
118
119            if (depth != 6) {
120                throw new Error("Cubemaps without all faces defined are not currently supported.");
121            }
122
123            image.setDepth(depth);
124        } else {
125            // make sure we have at least depth of 1.
126            image.setDepth(info.header.dwDepth > 0 ? info.header.dwDepth : 1);
127        }
128    }
129
130    private static final void populateImage(final Image image, final DdsImageInfo info, final LittleEndianDataInput in)
131            throws IOException {
132        final int flags = info.header.ddpf.dwFlags;
133
134        final boolean compressedFormat = isSet(flags, DdsPixelFormat.DDPF_FOURCC);
135        final boolean rgb = isSet(flags, DdsPixelFormat.DDPF_RGB);
136        final boolean alphaPixels = isSet(flags, DdsPixelFormat.DDPF_ALPHAPIXELS);
137        final boolean lum = isSet(flags, DdsPixelFormat.DDPF_LUMINANCE);
138        final boolean alpha = isSet(flags, DdsPixelFormat.DDPF_ALPHA);
139
140        if (compressedFormat) {
141            final int fourCC = info.header.ddpf.dwFourCC;
142            // DXT1 format
143            if (fourCC == getInt("DXT1")) {
144                info.bpp = 4;
145                // if (isSet(flags, DdsPixelFormat.DDPF_ALPHAPIXELS)) {
146                // XXX: many authoring tools do not set alphapixels, so we'll error on the side of alpha
147                logger.finest("DDS format: DXT1A");
148                image.setDataFormat(ImageDataFormat.PrecompressedDXT1A);
149                // } else {
150                // logger.finest("DDS format: DXT1");
151                // image.setDataFormat(ImageDataFormat.PrecompressedDXT1);
152                // }
153            }
154
155            // DXT3 format
156            else if (fourCC == getInt("DXT3")) {
157                logger.finest("DDS format: DXT3");
158                info.bpp = 8;
159                image.setDataFormat(ImageDataFormat.PrecompressedDXT3);
160            }
161
162            // DXT5 format
163            else if (fourCC == getInt("DXT5")) {
164                logger.finest("DDS format: DXT5");
165                info.bpp = 8;
166                image.setDataFormat(ImageDataFormat.PrecompressedDXT5);
167            }
168
169            // DXT10 info present...
170            else if (fourCC == getInt("DX10")) {
171                switch (info.headerDX10.dxgiFormat) {
172                    case DXGI_FORMAT_BC4_UNORM:
173                        logger.finest("DXGI format: BC4_UNORM");
174                        info.bpp = 4;
175                        image.setDataFormat(ImageDataFormat.PrecompressedLATC_L);
176                        break;
177                    case DXGI_FORMAT_BC5_UNORM:
178                        logger.finest("DXGI format: BC5_UNORM");
179                        info.bpp = 8;
180                        image.setDataFormat(ImageDataFormat.PrecompressedLATC_LA);
181                        break;
182                    default:
183                        throw new Error("dxgiFormat not supported: " + info.headerDX10.dxgiFormat);
184                }
185            }
186
187            // DXT2 format - unsupported
188            else if (fourCC == getInt("DXT2")) {
189                logger.finest("DDS format: DXT2");
190                throw new Error("DXT2 is not supported.");
191            }
192
193            // DXT4 format - unsupported
194            else if (fourCC == getInt("DXT4")) {
195                logger.finest("DDS format: DXT4");
196                throw new Error("DXT4 is not supported.");
197            }
198
199            // Unsupported compressed type.
200            else {
201                throw new Error("unsupported compressed dds format found (" + fourCC + ")");
202            }
203        }
204
205        // not a compressed format
206        else {
207            // TODO: more use of bit masks?
208            // TODO: Use bit size instead of hardcoded 8 bytes? (need to also implement in readUncompressed)
209            image.setDataType(PixelDataType.UnsignedByte);
210
211            info.bpp = info.header.ddpf.dwRGBBitCount;
212
213            // One of the RGB formats?
214            if (rgb) {
215                if (alphaPixels) {
216                    logger.finest("DDS format: uncompressed rgba");
217                    image.setDataFormat(ImageDataFormat.RGBA);
218                } else {
219                    logger.finest("DDS format: uncompressed rgb ");
220                    image.setDataFormat(ImageDataFormat.RGB);
221                }
222            }
223
224            // A luminance or alpha format
225            else if (lum || alphaPixels) {
226                if (lum && alphaPixels) {
227                    logger.finest("DDS format: uncompressed LumAlpha");
228                    image.setDataFormat(ImageDataFormat.LuminanceAlpha);
229                }
230
231                else if (lum) {
232                    logger.finest("DDS format: uncompressed Lum");
233                    image.setDataFormat(ImageDataFormat.Luminance);
234                }
235
236                else if (alpha) {
237                    logger.finest("DDS format: uncompressed Alpha");
238                    image.setDataFormat(ImageDataFormat.Alpha);
239                }
240            } // end luminance/alpha type
241
242            // Unsupported type.
243            else {
244                throw new Error("unsupported uncompressed dds format found.");
245            }
246        }
247
248        info.calcMipmapSizes(compressedFormat);
249        image.setMipMapByteSizes(info.mipmapByteSizes);
250
251        // Add up total byte size of single depth layer
252        int totalSize = 0;
253        for (final int size : info.mipmapByteSizes) {
254            totalSize += size;
255        }
256
257        // Go through and load in image data
258        final List<ByteBuffer> imageData = new ArrayList<>();
259        for (int i = 0; i < image.getDepth(); i++) {
260            // read in compressed data
261            if (compressedFormat) {
262                imageData.add(readDXT(in, totalSize, info, image));
263            }
264
265            // read in uncompressed data
266            else if (rgb || lum || alpha) {
267                imageData.add(readUncompressed(in, totalSize, rgb, lum, alpha, alphaPixels, info, image));
268            }
269        }
270
271        // set on image
272        image.setData(imageData);
273    }
274
275    static final ByteBuffer readDXT(final LittleEndianDataInput in, final int totalSize, final DdsImageInfo info,
276            final Image image) throws IOException {
277        int mipWidth = info.header.dwWidth;
278        int mipHeight = info.header.dwHeight;
279
280        final ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize);
281        for (int mip = 0; mip < info.header.dwMipMapCount; mip++) {
282            final byte[] data = new byte[info.mipmapByteSizes[mip]];
283            in.readFully(data);
284            if (!info.flipVertically) {
285                buffer.put(data);
286            } else {
287                final byte[] flipped = flipDXT(data, mipWidth, mipHeight, image.getDataFormat());
288                buffer.put(flipped);
289
290                mipWidth = Math.max(mipWidth / 2, 1);
291                mipHeight = Math.max(mipHeight / 2, 1);
292            }
293        }
294        buffer.rewind();
295        return buffer;
296    }
297
298    private static ByteBuffer readUncompressed(final LittleEndianDataInput in, final int totalSize,
299            final boolean useRgb, final boolean useLum, final boolean useAlpha, final boolean useAlphaPixels,
300            final DdsImageInfo info, final Image image) throws IOException {
301        final int redLumShift = shiftCount(info.header.ddpf.dwRBitMask);
302        final int greenShift = shiftCount(info.header.ddpf.dwGBitMask);
303        final int blueShift = shiftCount(info.header.ddpf.dwBBitMask);
304        final int alphaShift = shiftCount(info.header.ddpf.dwABitMask);
305
306        final int sourcebytesPP = info.header.ddpf.dwRGBBitCount / 8;
307        final int targetBytesPP = ImageUtils.getPixelByteSize(image.getDataFormat(), image.getDataType());
308
309        final ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize);
310
311        int mipWidth = info.header.dwWidth;
312        int mipHeight = info.header.dwHeight;
313        int offset = 0;
314
315        for (int mip = 0; mip < info.header.dwMipMapCount; mip++) {
316            for (int y = 0; y < mipHeight; y++) {
317                for (int x = 0; x < mipWidth; x++) {
318                    final byte[] b = new byte[sourcebytesPP];
319                    in.readFully(b);
320
321                    final int i = getInt(b);
322
323                    final byte redLum = (byte) (((i & info.header.ddpf.dwRBitMask) >> redLumShift));
324                    final byte green = (byte) (((i & info.header.ddpf.dwGBitMask) >> greenShift));
325                    final byte blue = (byte) (((i & info.header.ddpf.dwBBitMask) >> blueShift));
326                    final byte alpha = (byte) (((i & info.header.ddpf.dwABitMask) >> alphaShift));
327
328                    if (info.flipVertically) {
329                        dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP);
330                    }
331
332                    if (useAlpha) {
333                        dataBuffer.put(alpha);
334                    } else if (useLum) {
335                        if (useAlphaPixels) {
336                            dataBuffer.put(redLum).put(alpha);
337                        } else {
338                            dataBuffer.put(redLum);
339                        }
340                    } else if (useRgb) {
341                        if (useAlphaPixels) {
342                            dataBuffer.put(redLum).put(green).put(blue).put(alpha);
343                        } else {
344                            dataBuffer.put(redLum).put(green).put(blue);
345                        }
346                    }
347                }
348            }
349
350            offset += mipWidth * mipHeight * targetBytesPP;
351
352            mipWidth = Math.max(mipWidth / 2, 1);
353            mipHeight = Math.max(mipHeight / 2, 1);
354        }
355
356        return dataBuffer;
357    }
358
359    private final static class DdsImageInfo {
360        boolean flipVertically;
361        int bpp = 0;
362        DdsHeader header;
363        DdsHeaderDX10 headerDX10;
364        int mipmapByteSizes[];
365
366        void calcMipmapSizes(final boolean compressed) {
367            int width = header.dwWidth;
368            int height = header.dwHeight;
369            int size = 0;
370
371            mipmapByteSizes = new int[header.dwMipMapCount];
372
373            for (int i = 0; i < header.dwMipMapCount; i++) {
374                if (compressed) {
375                    size = ((width + 3) / 4) * ((height + 3) / 4) * bpp * 2;
376                } else {
377                    size = width * height * bpp / 8;
378                }
379
380                mipmapByteSizes[i] = ((size + 3) / 4) * 4;
381
382                width = Math.max(width / 2, 1);
383                height = Math.max(height / 2, 1);
384            }
385        }
386    }
387}