-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdds_tutorial.htm
More file actions
358 lines (322 loc) · 25 KB
/
Copy pathdds_tutorial.htm
File metadata and controls
358 lines (322 loc) · 25 KB
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
<html>
<head>
<title>OpenGL - LWJGL DDS texture compression tutorial</title>
<meta name="author" content="Christian Cohnen">
<meta name="description" content="OpenGL - LWJGL DDS texture compression tutorial">
<meta name="keywords" content="opengl, lwjgl, dds, texture, compression, dxt1, dxt3, dxt5, cubemap, tutorial">
<style type="text/css">
<!--
.style2 {font-size: smaller}
.javacode {
color:#333333;
background-color: #CCCCCC;
FONT-STYLE: normal;
FONT-FAMILY: "Courier New", Courier, mono;
FONT-VARIANT: normal;
LINE-HEIGHT: normal;
TEXT-DECORATION: none;
}
.javacomment { color:#0000FF ; background-color:#CCCCCC}
.default { color:#000000 ; background-color:#FFFFFF}
.style3 {color: #666666}
.style4 {
background-color: #CCCCCC;
color: #000000;
}
.style5 {color: #000000}
.style6 {color: #FF0000}
.style7 {color: #0000FF}
-->
</style>
</head>
<h1 align="center">OpenGL - LWJGL <br>
DDS
texture compression <br>
coding tutorial<br>
<span class="style2">by Christian Cohnen<br>
<span class="style7">chris@chriscohnen.de </span> </span></h1>
<p> </p>
<p>relevant keywords: <br>
<span class="style2">glCompressedTexImage2DARB <br>
GL_COMPRESSED_RGBA_S3TC_DXT1_EXT <br>
GL_COMPRESSED_RGBA_S3TC_DXT3_EXT <br>
GL_COMPRESSED_RGBA_S3TC_DXT5_EXT <br>
DDSURFACEDESC2</span></p>
<h2>Introduction</h2>
<p align="justify">Many people are starting to play around with Java and OpenGL using <a href="http://www.lwjgl.org">LWJGL</a> (like me). This small tutorial is directed at this audience, especially if they do not want to dig into Microsoft DirectX files
themselves. You can waste your time better than this.
I did take me quite some hours to find all the necessary info in the net to get myself started on a DDS file loader.<br>
Of course
there are lot of useful texture formats like TGA, PNG and JPG that are not
platform
dependent
and M$ proprietary but that does not count here. Full/direct DDS support is quite useful if you
don't want to break your tool chain. And DDS has quite good support in the windows world. For example plugins for 3DSMax and Photoshop<a href="#ref3">[3]</a> are available and the format is natively used in tools from ATI,NVIDIA and others for normal map generation. Compared to formats like TGA a plus for DDS is support for texture compression, and storing cubemaps and mipmap levels in the same file. <br>
</p>
<p align="justify">Starting point for getting into texture compression with DDS for me was this nice NVIDIA <a href="#ref1">document [1]</a> explaining the OpenGL extension for texture compression. Most of the java code was converted from the example given in the document. </p>
<p>If you need to uncompress and compress images in DDS format have a look at the DevIL image library C source. It supports DXT1/3/5
uncompressing. You can find DevIL <a href="#ref2">here [2]</a>. </p>
<h2>Simple DDSReader</h2>
<p class="default">So let`s start with the code. I implemented the loader in a
separate
class called <strong>DDSReader</strong>. The class also has one inner class to represent the DDS file header and makes use of a BinaryFileReader class for easy file handling.
I have some lwjgl imports at the start of the DDSReader file. </p>
<p class="javacode" style="background-color:#CCCCCC ">import java.nio.IntBuffer;<br>
import java.nio.ByteBuffer;<br>
import org.lwjgl.opengl.*;<br>
import org.lwjgl.BufferUtils;<br>
<br>public class DDSReader {<br>
<p align="justify" >
<span class="default">
Loading the binary file is done via a Binary File Reader class that loads the file into memory and then give you access to the file content using
different
data return types. This is quite handy. You can substitute this easily with your own implementation.
The BinaryFileReader is declared at the beginning. </span>
<p align="justify" class="javacode" style="background-color:#CCCCCC ">
BinaryFileReader bis;
</p>
<p class="default"> A DDS file stores all relevant information in the file header. Format of this header is defined the Microsoft <strong>ddraw.h</strong> file as struct _DDSURFACEDESC2.
We use a inner class for storing the header information that more or less corresponds to the _DDSURFACEDESC2 struct and sub structures DDPIXELFORMAT,DDSCAPS2 but is very simplified, because we only need it for loading.. We also have to declare some
constant values that are required when fiddling around with the header flags. For a short explanation of the header values see java comments in the code:<br>
</p>
<p align="justify" class="javacode" style="background-color:#CCCCCC ">static final long DDSD_MIPMAPCOUNT = 0x00020000l;<br>
static final long DDSCAPS_COMPLEX = 0x00000008l;<br>
static final long DDSCAPS2_CUBEMAP = 0x00000200l;<br>
static final long DDSCAPS2_CUBEMAPSIDE[] = {0x00000400L, 0x00000800L, 0x00001000L, 0x00002000L, 0x00004000L, 0x00008000L};</p>
<p class="javacode">class DDSURFACEDESC2 {<br>
<span class="style3">/** size of the DDSURFACEDESC structure */ </span><span class="style5">int size;</span><br>
<span class="style3">/** determines what fields are valid*/ <span class="style5">int flags;</span><br>
/** height of surface to be created */ <span class="style5">int height;</span><br>
/** width of input surface*/ <span class="style5">int width;</span><br>
/** formless optimized surface size */ <span class="style5">int linearSize;</span><br>
/** the depth, if volume texture */ <span class="style5">int depth;</span><br>
/** number of mip-map levels */ <span class="style5">int mipMapCount;</span><br>
/** depth of alpha buffer */ <span class="style5">int dwAlphaBitDepth;</span><br>
// DDPIXELFORMAT pixelFormat pixel format ddsheader of the surface<br>
/** size of structure */ <span class="style5">int size2;</span><br>
/** pixel format flags */ <span class="style5">int flags2;</span><br>
/**(FOURCC code) */ <span class="style5">String fourCC;</span><br>
/** how many bits per pixel */ <span class="style5">int rgbBitCount;</span><br>
/** mask for red bit */ <span class="style5">int rBitMask;</span><br>
/** mask for green bits */ <span class="style5">int gBitMask;</span><br>
/** mask for blue bits */ <span class="style5">int bBitMask;</span><br>
/** mask for alpha channel */ <span class="style5">int rgbAlphaBitMask;</span><br>
// DDPIXELFORMAT pixelFormat end<br>
// DDSCAPS2 ddsCaps direct draw surface capabilities<br>
<span class="style5">int caps1;</span><br>
<span class="style5">int caps2;</span><br>
<span class="style5">int caps3;</span><br>
/* dwVolumeDepth */ <span class="style5">int caps4;</span><br>
<span class="style4"> int dwTextureStage; </span></span><br>
</span></p>
<p><span class="default">It is very easy to read the header as it has a fixed size of 128 bytes. It always starts with the 4 characters "DDS_" identifier.<br>
Here is an example DDS header. I marked the identifier, the number of mipmap levels and the fourCC (four character code indicating the compression format):</span></p>
<p>00000000h: <span class="style6">44 44 53</span> 20 7C 00 00 00 07 10 0A 00 00 02 00 00 ; <span class="style6">DDS</span> |...........<br>
00000010h: 00 02 00 00 00 00 02 00 00 00 00 00 <span class="style6">0A</span> 00 00 00 ; ................<br>
00000020h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................<br>
00000030h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................<br>
00000040h: 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 00 ; ............ ...<br>
00000050h: 04 00 00 00 <span class="style6">44 58 54 31</span> 00 00 00 00 00 00 00 00 ; ....<span class="style6">DXT1</span>........</p>
<p>the following method reads the header variables (size 124 bytes) behind the DDS identifier.</p>
<p class="javacode">public void read() { <br>
size = bis.readInt();<br>
flags = bis.readInt();<br>
height = bis.readInt();<br>
width = bis.readInt();<br>
linearSize = bis.readInt();<br>
depth = bis.readInt();<br>
mipMapCount = bis.readInt();<br>
dwAlphaBitDepth = bis.readInt();<br>
<span class="style3"> // skip until DDPixelformat</span><br>
bis.setIndex(bis.getIndex()+40);<br>
<span class="style3">// DDPIXELFORMAT 2</span><br>
size2 = bis.readInt();<br>
flags2 = bis.readInt();<br>
fourCC = bis.readString(4);<br>
rgbBitCount = bis.readInt();<br>
rBitMask = bis.readInt();<br>
gBitMask = bis.readInt();<br>
bBitMask = bis.readInt();<br>
rgbAlphaBitMask = bis.readInt();<br>
<span class="style3"> // DDCAPS2 struct </span><br>
caps1 = bis.readInt();<br>
caps2 = bis.readInt();<br>
caps3 = bis.readInt();<br>
caps4 = bis.readInt();<br>
<span class="style3">// DDCAPS2 end</span><br>
dwTextureStage = bis.readInt();<br>
}<br>
</p>
<p>At this point we can start explaining the loader method. It takes the filename of the DDS file. First we test if the DDS identifier is ok then we load the header from above. </p>
<p class="javacode"><span class="style3">/**<br>
* load DDS file<br>
* @param filename<br>
*/</span><br>
public void loadDDSFile(String filename) {<br>
boolean isCubemap=false;<br>
<br>
bis = new BinaryFileReader(filename);<br>
if ((bis != null) && (bis.isReadOK())) {<br>
String filetype = bis.readString(4);<br>
if (!"DDS ".equals(filetype)) {<br>
System.out.println("not dds format");<br>
return;<br>
}<br>
ddsheader.read();<br></p>
<p>We also check the size variables of the structure these must always be 124 and 32, if values are ok we set the file pointer to the end of the header and printout the header information. </p>
<p class="javacode">
if (ddsheader.size!=124) { System.out.println("not dds format"); return; }<br>
if (ddsheader.size2!=32) { System.out.println("not dds format"); return; }<br>
bis.setIndex(ddsheader.size + 4);<span class="style3"> // seek to texture data, pos= filetype ( is 4)+ ddsheader (is 124) </span><br>
System.out.println(ddsheader);<br>
</p>
<p>In order to determine the compression used in the DDS data for all surfaces we check the four character code. This should be DXT1, DXT3 or DXT5 (see NVIDIA document [1]). There are also other fourCCs like <em>ATI2 (for 3Dc compression)</em> and so on, but you have to add code yourself if you need to support those compressions. </p>
<p class="javacode">
int format = 0;<br>
<br>
if ("DXT1".equals(ddsheader.fourCC)) {<br>
format = EXTTextureCompressionS3TC.GL_COMPRESSED_RGB_S3TC_DXT1_EXT;<br>
}<br>
if ("DXT3".equals(ddsheader.fourCC)) {<br>
format = EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;<br>
}<br>
if ("DXT5".equals(ddsheader.fourCC)) {<br>
format = EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;<br>
}<br>
</p>
<p> Next thing to do is calculate the size of the first surface stored in the DDS texture, depth should be 1 if this is a 2D texture. Compression uses blocks to store the image. One block represents 16 texels (4x4 tile). Depending on the compression format the blocksize is 8 (DXT1) or 16 (DXT3,DXT5) bytes. Size of the encoded image is <em>ceil(width/4)*ceil(height/4)*blocksize</em> see [3] . </p>
<p class="javacode">
int blocksize=16;<br>
if (format == EXTTextureCompressionS3TC.GL_COMPRESSED_RGB_S3TC_DXT1_EXT) blocksize = 8;<br>
int size = ((ddsheader.width + 3)/4) * ((ddsheader.height + 3)/4) * ((ddsheader.depth + 3)/4)*blocksize;<br>
</p>
<p>To find out, if the DDS file stores a cubemap we check the caps2 field from the header and we fix missing mip map count value as well </p>
<p class="javacode">
if ((ddsheader.caps1 & (DDSCAPS_COMPLEX)) > 0)<br>
if ((ddsheader.caps2 & DDSCAPS2_CUBEMAP) > 0) {<br>
isCubemap = true;<br>
}<br>
if (((ddsheader.flags & DDSD_MIPMAPCOUNT) == 0) || ddsheader.mipMapCount == 0) {<br>
ddsheader.mipMapCount = 1;<br>
}<br>
if (isCubemap) loadCubeMapTexture(size, format, blocksize,ddsheader.caps2);<br>
else load2DTexture(size, format, blocksize);<br>
}<span class="style3">// end of loadDDSFile(String filename) </span><br>
</p>
<p align="left">If it is a cubemap, we finally call the <em>loadCubemap(int size,int format,int blocksize)</em> method otherwise we call <em>load2DTexture(int size,int format,int blocksize)<br>
</em>
I'll explain load2DTexture first:<br><br>
In LWJGL we need to create an IntBuffer to hold the texture address that is returned from the GL11.glGenTextures function. As can be seen in LWJGL code looks nearly 1:1 to the standard OpenGL code. The address returned is used in the glBindTexture method. </p>
<p class="javacode">
private void load2DTexture(int size, int format, int blocksize) {<br>
IntBuffer buf = BufferUtils.createIntBuffer(1);<br>
GL11.glGenTextures(buf); // Create Texture In OpenGL<br>
GL11.glBindTexture(GL11.GL_TEXTURE_2D, buf.get(0));<br>
int dds_compressed_decal_map = buf.get(0);<br>
</p>
<p>Next part reads the compressed data from the BinaryFileReader at the current index with the calculated size and puts it into a fresh ByteBuffer scratch.The scratch needs to be rewinded else we get a LWJGL crash.</p>
<p class="javacode">
ByteBuffer pixBuf = BufferUtils.createByteBuffer(size);<br>
pixBuf.put(bis.contents,bis.getIndex(),size);<br>
bis.setIndex(bis.getIndex()+size);<br>
pixBuf.rewind();
</p>
<p class="default">
Before we create the texture with the compressed data we set the OpenGL filter values otherwise we would end with a white texture. The GL13.glCompressedTexImage2D
call generates the texture for mip level 0. The compression format parsed into the format variable (see above) is used here.
</p>
<p class="javacode">
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);<br>
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);<br>
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);<br>
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);<br>
GL13.glCompressedTexImage2D(GL11.GL_TEXTURE_2D, 0, format, ddsheader.width, ddsheader.height, 0,size, pixBuf);<br>
</p>
<p class="default">
When the header indicates that we have mip levels stored in the file we also generate the mip map levels. For each mip level we retrieve the compressed data from the BinaryFileReader.
The width and height is always divided by 2 and we recalculate the size. Please note that all levels down to 1x1 pixel need to be generated, otherwise we end with a white texture.
</p><p class="javacode">
// set mipmap Filtering and load all levels<br>
if (ddsheader.mipMapCount > 1) {<br>
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);<br>
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);<br>
int width = ddsheader.width;<br>
int height = ddsheader.height;<br>
for (int i = 1; i < ddsheader.mipMapCount; i++) {<br>
width >>= 1;<br>
height >>= 1;<br>
if (width == 0) width = 1;<br>
if (height == 0) height = 1;<br>
size = ((width + 3) / 4) * ((height + 3) / 4) * ((ddsheader.depth + 3) / 4) * blocksize;<br>
pixBuf.put(bis.contents,bis.getIndex(),size);<br>
bis.setIndex(bis.getIndex()+size);<br>
pixBuf.rewind();<br>
GL13.glCompressedTexImage2D(GL11.GL_TEXTURE_2D, i, format, width, height, 0, size, pixBuf);<br>
}<br>
}<br>
}<br>
</p>
</p>
<p>That's it, we have loaded the surface with all mip levels into an OpenGL texture. If the DDS header
indicates
a cubemap we call a different method: <em>loadCubeMapTexture(...)</em>.</p>
<p> The method loadCubeMapTexture is nearly the same as load2DTexture(...) we just have a loop for the cubeside. For cubemaps we load up to six surfaces (and their mip levels) from the dds file. The cube surfaces are stored sequentially without data between them. The header caps2 value indicates which sides are present and is passed as a parameter to the method. </p>
<p class="default">
</p><p class="javacode">
private void loadCubeMapTexture(int size, int format, int blocksize,int caps2) {<br>
IntBuffer buf = BufferUtils.createIntBuffer(1);<br>
GL11.glGenTextures(buf);<br>
// create cube texture<br>
GL11.glBindTexture(GL13.GL_TEXTURE_CUBE_MAP, buf.get(0));<br>
<br>
ByteBuffer pixBuf = BufferUtils.createByteBuffer(size);<br>
for (int side = 0; side < 6; side++) {<br>
if (!((caps2 & DDSCAPS2_CUBEMAPSIDE[side]) > 0)) {<br>
continue;<br>
}<br>
<br>
// load level 0<br>
<br>
pixBuf.put(bis.contents, bis.getIndex(), size);<br>
bis.setIndex(bis.getIndex() + size);<br>
pixBuf.rewind();<br>
GL11.glTexParameteri(GL13.GL_TEXTURE_CUBE_MAP, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);<br>
GL11.glTexParameteri(GL13.GL_TEXTURE_CUBE_MAP, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);<br>
GL13.glCompressedTexImage2D(GL13.GL_TEXTURE_CUBE_MAP_POSITIVE_X+side, 0, format, ddsheader.width, ddsheader.height, 0, size, pixBuf);<br>
<br>
// set mipmap filtering and load all levels<br>
if (ddsheader.mipMapCount > 1) {<br>
GL11.glTexParameteri(GL13.GL_TEXTURE_CUBE_MAP, GL11.GL_TEXTURE_MIN_FILTER,GL11.GL_LINEAR_MIPMAP_LINEAR);<br>
GL11.glTexParameteri(GL13.GL_TEXTURE_CUBE_MAP, GL11.GL_TEXTURE_MAG_FILTER,GL11.GL_LINEAR);<br>
int width = ddsheader.width;<br>
int height = ddsheader.height;<br>
for (int i = 1; i < ddsheader.mipMapCount; i++) {<br>
width >>= 1;<br>
height >>= 1;<br>
if (width == 0) width = 1;<br>
if (height == 0) height = 1;<br>
int mipsize = ((width + 3) / 4) * ((height + 3) / 4) * ((ddsheader.depth + 3) / 4) * blocksize;<br>
pixBuf.put(bis.contents, bis.getIndex(), mipsize);<br>
bis.setIndex(bis.getIndex() + mipsize);<br>
pixBuf.rewind();<br>
<br>
GL13.glCompressedTexImage2D(GL13.GL_TEXTURE_CUBE_MAP_POSITIVE_X + side, i, format, width, height, 0, mipsize, pixBuf);<br>
}<br>
}<br>
}<br>
}<br>
</p>
<p class="default">Before processing the six surface we create the cube map by specifying GL13.GL_TEXTURE_CUBE_MAP in the glBindTexture call. For all surfaces we reuse one ByteBuffer scratch in the maximum size we have to handle. For the smaller sub surfaces (mip levels) we use the buffer but set how much of the data is valid when calling the glCompressedTexImage2D function. <br>
<br>
In the simple example code (see download) a small main applet uses the DDSReader class to load a single DDS file. It draws the texture on the screen in different sizes triggering the mip levels. If the DDS file stores a cube map all six side are shown.
<p class="default">
<h2>Conclusion</h2>
<p>This tutorial should give you enough information to add support for DDS files in your LWJGL based app or 3D engine. Further improvement could be support for normal map compression and faster IO using New IO (NIO).</p>
<p>DOWNLOAD <a href="ddstutorial.zip"> DDS tutorial source code</a> (including this document).</p>
<p>Please send any comments to chris@chriscohnen.de</p>
<h2>References</h2>
<p><a name="ref1"></a>[1] <a href="http://developer.nvidia.com/attach/6585">NVIDIA Using Texture Compression in OpenGL PDF </a></p>
<p><a name="ref2"></a>[2] <a href="http://openil.sourceforge.net">Developer's Image Library (DevIL)</a> </p>
<p><a name="ref3"></a>[3] <a href="http://developer.nvidia.com/object/nv_texture_tools.html">NVIDIA Texture Tools (Photoshop plugin)</a></p>
<p> </p>
<p class="style2">© 2004 Christian Cohnen. All rights reserved.</p>