Code archives/File Utilities/SaveImageDDS function
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
Code from my SaveImageDDS thread. This function supports saving to dxt1, dxt3 and dxt5 as well as 24-bit uncompressed. It also seems to work for dxt2 and dxt4, although probably not quite right. It uses an optional alpha image map to determine the alpha texels. This allows you to have full control over things. If you don't want to use an alpha map you can just omit the alpha parameter. Updated by Tom C. to support mipmaps! | |||||
;SaveImageDDS example code Graphics3D 640,480,0,2 ;Setup the scene AppTitle "Save Image DDS" SetBuffer BackBuffer() camera=CreateCamera() light=CreateLight() RotateEntity light,45,45,0 image=MakeTestImage(128,128) ;Create a test image ;image=LoadImage("test.bmp") ;Alternatively load a bmp ;image=LoadImage("test.png") ;Alternatively load a png ;image=LoadImage("test.jpg") ;Alternatively load a jpg alpha=MakeTestAlpha(ImageWidth(image),ImageHeight(image)) ;A test alpha filename$="test.dds" ;DDS Filename format=5 ;Set DDS format ;alpha=0 ;If we don't want an alpha channel set "alpha" to 0 ok=SaveImageDDS(image,filename$,format,alpha) cube=CreateCube() ;Create a cube to texture PositionEntity cube,0,0,4 If format=1 Then flags=1+4 Else flags=1+2 ;Set either masked/alpha flags tex=LoadTexture(filename$,flags) ;Now load the new DDS file If format=1 Or format=3 Or format=5 EntityTexture cube,tex,0,0 ;Only texture cube if valid format EndIf cube2=CreateCube() ;Create a cube behind the first cube PositionEntity cube2,0,2,8 While Not KeyHit(1) ;Main loop RenderWorld() TurnEntity cube,0.4,0.3,0.2 ;Rotate the cubes TurnEntity cube2,-0.4,-0.3,-0.2 If space ;If space key was hit DrawImage image,50,50 ;Draw image If alpha DrawImage alpha,ImageWidth(image)+100,50 ;Draw alpha image EndIf If KeyHit(57) Then space=Not space ;Space key Text 0,0,"Hit space to show image and alpha maps" Text 0,20,"ok="+ok+" file="+filename+" format="+format+" alpha="+alpha Flip Wend Function MakeTestImage(width,height) Local image,x,y,rgb image=CreateImage(width,height) LockBuffer(ImageBuffer(image)) For y=0 To ImageHeight(image)-1 For x=0 To ImageWidth(image)-1 rgb=y+(y*256)+(x*256^2) ;Gradient color WritePixelFast x,y,rgb,ImageBuffer(image) Next Next UnlockBuffer(ImageBuffer(image)) SetBuffer ImageBuffer(image) Color 255,255,255 : Oval 40,40,30,30 Color 0,0,0 : Text 50,50,"DXTC" : Color 255,255,255 SetBuffer BackBuffer() Return image End Function Function MakeTestAlpha(width,height) Local alpha,x,y,rgb alpha=CreateImage(width,height) LockBuffer(ImageBuffer(alpha)) For y=0 To ImageHeight(alpha)-1 For x=0 To ImageWidth(alpha)-1 rgb=(y*2)+((y*2)*256)+((y*2)*256^2) ;Grayscale If Not x Mod 8 Then rgb=$7F7F7F ;Grid lines If Not y Mod 8 Then rgb=$7F7F7F If Not x Mod 16 Then rgb=$FFFFFF If Not y Mod 16 Then rgb=$FFFFFF WritePixel x,y,rgb,ImageBuffer(alpha) Next Next UnlockBuffer(ImageBuffer(alpha)) Return alpha End Function ;SaveImageDDS, by markcw on 13 Sep 06 ;MipMap support, by Tom C. on 12 Jul 07 ;Description: ;This function works like SaveImage except it saves DDS files. ;Set the dxt mode with the format parameter 1-5 (or 0 for uncompressed RGB only) ;the alphaimg parameter is for an alpha image. ;If you omit it (or set it to 0) you get no alpha. ;Of course you can use the same image handle for alpha image ;the last optional parameter indicates a mipmap generation, ;0 for all possible mipmaps, 1 for only the main image (default) ;Return value is False if fails, True if succeeds. Function SaveImageDDS(image,filename$,format,alphaimg=0,mipmaps=1) ;image=image handle, filename$, format=optional compression format 0..5 ;alphaimg=optional alpha image handle, zero if no alpha required ;mipmaps=count of mipmaps, if given the main picture will scaled down and saved to the file ;Uses MakeEmptyAlpha, MAX, MakeFourCC, ColorHighest, ColorTotal, Color565 Local width=ImageWidth(image) Local height=ImageHeight(image) Local imagebuf,alphabuf ;DXT3/5, if no alpha image then create empty white If alphaimg=0 And format<>1 Then alphaimg=MakeEmptyAlpha(width,height) alphaimg_created=True EndIf ;Determine mipmap count If mipmaps=0 Then c=MAX(width,height) mipmaps=Log(c)/Log(2)+1 EndIf Local dwwidth,dwheight,flags1,flags2,caps1,caps2,bpp,pitch,sizebytes,w,h,mipsize Local bsize,bindex,fourcc,hdds,i,x,y,offset,mipoffset,ix,iy,argb Local color0,color1,color2,color3,color4,color5,color6,color7 Local d0,d1,d2,d3,d4,d5,d6,d7,texel,file Local sclX#,sclY# ;dwFlags constants Local DDSD_CAPS=$00000001,DDSD_HEIGHT=$00000002,DDSD_WIDTH=$00000004 Local DDSD_PITCH=$00000008,DDSD_PIXELFORMAT=$00001000 Local DDSD_MIPMAPCOUNT=$00020000,DDSD_LINEARSIZE=$00080000 Local DDSD_DEPTH=$00800000,DDPF_ALPHAPIXELS=$00000001 Local DDPF_FOURCC=$00000004,DDPF_RGB=$00000040 ;dwCaps1 constants Local DDSCAPS_COMPLEX=$00000008,DDSCAPS_TEXTURE=$00001000 Local DDSCAPS_MIPMAP=$00400000 ;dwCaps2 constants Local DDSCAPS2_CUBEMAP=$00000200,DDSCAPS2_CUBEMAP_POSITIVEX=$00000400 Local DDSCAPS2_CUBEMAP_NEGATIVEX=$00000800 Local DDSCAPS2_CUBEMAP_POSITIVEY=$00001000 Local DDSCAPS2_CUBEMAP_NEGATIVEY=$00002000 Local DDSCAPS2_CUBEMAP_POSITIVEZ=$00004000 Local DDSCAPS2_CUBEMAP_NEGATIVEZ=$00008000,DDSCAPS2_VOLUME=$00200000 ;Calculate DWORD-aligned width and height, multiple of 4 dwwidth=(width+3)/4*4 dwheight=(height+3)/4*4 ;Default flags for all formats flags1=DDSD_CAPS Or DDSD_HEIGHT Or DDSD_WIDTH Or DDSD_PIXELFORMAT caps1=DDSCAPS_TEXTURE If format<=0 Or format>5 ;Uncompressed flags1=flags1 Or DDSD_PITCH flags2=DDPF_RGB bpp=24 ;Determine Size of Bytes for each mipmap and add w=dwwidth:h=dwheight For i=1 To mipmaps mipsize=MAX(1,w)*(bpp/8) ;DWORD-aligned scanline sizebytes=sizebytes+mipsize*h w=MAX(1,w/2):h=MAX(1,h/2) ;pitch If i=1 Then pitch=mipsize Next Else ;Compressed flags1=flags1 Or DDSD_LINEARSIZE flags2=DDPF_FOURCC ;Determine Size of Bytes for each mipmap and add If format>1 Then bytesC=16 Else bytesC=8 w=dwwidth:h=dwheight For i=1 To mipmaps mipsize=(MAX(1,w/4)*MAX(1,h/4))*bytesC sizebytes=sizebytes+mipsize w=MAX(1,w/2):h=MAX(1,h/2) ;Linear size If i=1 Then pitch=mipsize Next ;pitch=sizebytes bsize=2 : bindex=0 : If format>1 Then bsize=4 : bindex=8 ;block values If format=1 Then fourcc=MakeFourCC("D","X","T","1") If format=2 Then fourcc=MakeFourCC("D","X","T","2") If format=3 Then fourcc=MakeFourCC("D","X","T","3") If format=4 Then fourcc=MakeFourCC("D","X","T","4") If format=5 Then fourcc=MakeFourCC("D","X","T","5") EndIf hdds=CreateBank(128+sizebytes) ;Bank to store DDS ;Magic Value, DWORD PokeInt hdds,0,MakeFourCC("D","D","S"," ") ;dwMagic, "DDS " ;Surface Format Header, DDSURFACEDESC2 structure PokeInt hdds,4,124 ;dwSize, sizeof(DDSURFACEDESC2) PokeInt hdds,8,flags1 ;dwFlags, flags to indicate valid fields PokeInt hdds,12,dwheight ;dwHeight, image height in pixels PokeInt hdds,16,dwwidth ;dwWidth, image width in pixels PokeInt hdds,20,pitch ;dwPitchOrLinearSize, pitch or linear size PokeInt hdds,24,0 ;dwDepth, volume textures not supported until DX 8.0 PokeInt hdds,28,mipmaps ;dwMipMapCount, for items with mipmap levels For i=1 To 11 PokeInt hdds,(i*4)+28,0 ;dwReserved[11] Next ;DDPIXELFORMAT structure PokeInt hdds,76,32 ;dwSize, sizeof(DDPIXELFORMAT) PokeInt hdds,80,flags2 ;dwFlags, flags to indicate valid fields PokeInt hdds,84,fourcc ;dwFourCC PokeInt hdds,88,bpp ;dwRGBBitCount PokeInt hdds,92,$00FF0000 ;dwRBitMask PokeInt hdds,96,$0000FF00 ;dwGBitMask PokeInt hdds,100,$000000FF ;dwBBitMask PokeInt hdds,104,$FF000000 ;dwRGBAlphaBitMask ;DDCAPS2 structure PokeInt hdds,108,caps1 ;dwCaps1, flags to indicate valid fields PokeInt hdds,112,caps2 ;dwCaps2, flags to indicate valid fields For i=1 To 2 PokeInt hdds,(i*4)+112,0 ;dwReserved[2] Next PokeInt hdds,124,0 ;dwReserved2 mipoffset=128 ;default Offset after Header ;Main Surface Data, BYTE bData1[] and Attached Surfaces Data for MIPMAPS If format<=0 Or format>5 ;uncompressed, 24-bit For mips=1 To mipmaps ;Loop of optional mipmap count If mips>1 Then ;Store offset mipoffset=offset+pitch ;Half of dimension w=MAX(1,width/2):h=MAX(1,height/2) sclX#=Float#(w)/Float#(width):sclY#=Float#(h)/Float#(height) ScaleImage image,sclX#,sclY# width=w:height=h pitch=width*(bpp/8) EndIf ;Create Buffer imagebuf=ImageBuffer(image) LockBuffer(imagebuf) For y=0 To height-1 offset=mipoffset+pitch*y ;next DWORD-aligned scanline For x=0 To width-1 argb=ReadPixelFast(x,y,imagebuf) PokeByte hdds,offset+(x*3),argb And $000000FF ;b PokeByte hdds,offset+(x*3)+1,(argb And $0000FF00) Shr 8 ;g PokeByte hdds,offset+(x*3)+2,(argb And $00FF0000) Shr 16 ;r Next Next UnlockBuffer(imagebuf) Next Else ;Compressed For mips=1 To mipmaps ;Loop of optional mipmap count If mips>1 Then ;Store offset mipoffset=offset+dwwidth*bsize ;Half of dimension w=MAX(1,width/2):h=MAX(1,height/2) sclX#=Float#(w)/Float#(width):sclY#=Float#(h)/Float#(height) ScaleImage image,sclX#,sclY# ;If not the same image-handle scale the alpha image too If image<>alphaimg And alphaimg>0 Then ScaleImage alphaimg,sclX#,sclY# width=w:height=h ;Calculate DWORD-aligned width and height, multiple of 4 dwwidth=(width+3)/4*4:dwheight=(height+3)/4*4 EndIf ;Create Buffer imagebuf=ImageBuffer(image) If alphaimg Then alphabuf=ImageBuffer(alphaimg) LockBuffer(imagebuf) LockBuffer(alphabuf) For y=0 To dwheight-1 Step 4 offset=mipoffset+MAX(1,dwwidth/4)*(y*bsize) ;Next block-aligned scanline For x=0 To dwwidth-1 Step 4 If format=2 Or format=3 ;DXT2,DXT3 ;Find color in alpha block and set each alpha texel For iy=0 To 3 For ix=0 To 3 If x+ix<width And y+iy<height ;Not out of bounds argb=ReadPixelFast(ix+x,iy+y,alphabuf) ;Use alpha map Else argb=0 ;Black EndIf i=ColorHighest(argb)/17 : If i>15 Then i=15 ;Alpha color 0..15 texel=PeekShort(hdds,offset+(x*bsize)+(iy*2)) Or (i Shl ix*4) PokeShort hdds,offset+(x*bsize)+(iy*2),texel ;wAlphaTexels[4] Next Next EndIf If format=4 Or format=5 ;DXT4,DXT5 ;Find highest and lowest colors in alpha block color0=0 : color1=$FFFFFFFF ;color0 highest For iy=0 To 3 For ix=0 To 3 If x+ix<width And y+iy<height ;Not out of bounds argb=ReadPixelFast(ix+x,iy+y,alphabuf) ;Use alpha map If ColorHighest(argb)>ColorHighest(color0) Then color0=argb If ColorHighest(argb)<ColorHighest(color1) Then color1=argb EndIf Next Next ;Make sure color0 is the highest If color1>color0 Then i=color0 : color0=color1 : color1=i ;Switch order EndIf PokeByte hdds,offset+(x*bsize),ColorHighest(color0) ;bAlpha0 PokeByte hdds,offset+(x*bsize)+1,ColorHighest(color1) ;bAlpha1 ;Set each alpha texel in block to closest alpha color0=ColorHighest(color0) : color1=ColorHighest(color1) For iy=0 To 3 For ix=0 To 3 If x+ix<width And y+iy<height ;Not out of bounds argb=ReadPixelFast(ix+x,iy+y,alphabuf) ;Use alpha map Else argb=0 ;black EndIf If color0>color1 ;8-alpha block color2=((6*color0)+color1)/7 color3=((5*color0)+(2*color1))/7 color4=((4*color0)+(3*color1))/7 color5=((3*color0)+(4*color1))/7 color6=((2*color0)+(5*color1))/7 color7=(color0+(6*color1))/7 Else ;6-alpha block color2=((4*color0)+color1)/5 color3=((3*color0)+(2*color1))/5 color4=((2*color0)+(3*color1))/5 color5=(color0+(4*color1))/5 color6=0 color7=255 EndIf d0=Abs(color0-ColorHighest(argb)) ;Get differences d1=Abs(color1-ColorHighest(argb)) d2=Abs(color2-ColorHighest(argb)) d3=Abs(color3-ColorHighest(argb)) d4=Abs(color4-ColorHighest(argb)) d5=Abs(color5-ColorHighest(argb)) d6=Abs(color6-ColorHighest(argb)) d7=Abs(color7-ColorHighest(argb)) i=0 : If d1<d0 Then d0=d1 : i=1 ;Find closest color If d2<d0 Then d0=d2 : i=2 If d3<d0 Then d0=d3 : i=3 If d4<d0 Then d0=d4 : i=4 If d5<d0 Then d0=d5 : i=5 If d6<d0 Then d0=d6 : i=6 If d7<d0 Then d0=d7 : i=7 If iy<2 ;Upper 24bit-block texel=PeekInt(hdds,offset+(x*bsize)+2) And $00FFFFFF If iy=0 Then texel=texel Or (i Shl (ix*3)) If iy=1 Then texel=texel Or (i Shl ((ix*3)+12)) PokeInt hdds,offset+(x*bsize)+2,texel And $00FFFFFF Else ;Lower 24bit-block texel=PeekInt(hdds,offset+(x*bsize)+5) And $00FFFFFF If iy=2 Then texel=texel Or (i Shl (ix*3)) If iy=3 Then texel=texel Or (i Shl ((ix*3)+12)) PokeInt hdds,offset+(x*bsize)+5,texel And $00FFFFFF EndIf Next Next EndIf ;Find highest and lowest colors in texel block ;Better algorithm might be to find the most common highest/lowest colors color0=0 : color1=$FFFFFFFF ;color0 highest For iy=0 To 3 For ix=0 To 3 If x+ix<width And y+iy<height ;Not out of bounds argb=ReadPixelFast(ix+x,iy+y,imagebuf) If ColorTotal(argb)>ColorTotal(color0) Then color0=argb If ColorTotal(argb)<ColorTotal(color1) Then color1=argb EndIf Next Next ;Make sure color0 is the highest If color1>color0 Then i=color0 : color0=color1 : color1=i ;Switch order EndIf ;Switch order, color1 highest to indicate DXT1a If format=1 And alphaimg<>0 ;DXT1a, using alpha bit i=color0 : color0=color1 : color1=i EndIf PokeShort hdds,offset+(x*bsize)+bindex,Color565(color0) ;wColor0 PokeShort hdds,offset+(x*bsize)+bindex+2,Color565(color1) ;wColor1 ;Set each texel in block to closest color color0=ColorTotal(color0) : color1=ColorTotal(color1) For iy=0 To 3 For ix=0 To 3 If x+ix<width And y+iy<height ;Not out of bounds argb=ReadPixelFast(ix+x,iy+y,imagebuf) Else argb=0 ;Black EndIf If color0>color1 ;Four-color block color2=((2*color0)+color1)/3 color3=(color0+(2*color1))/3 Else ;Three-color block color2=(color0+color1)/2 color3=3*16 ;Max transparent color EndIf d0=Abs(color0-ColorTotal(argb)) ;Get differences d1=Abs(color1-ColorTotal(argb)) d2=Abs(color2-ColorTotal(argb)) d3=Abs(color3-ColorTotal(argb)) i=0 : If d1<d0 Then d0=d1 : i=1 ;Find closest color If d2<d0 Then d0=d2 : i=2 If d3<d0 Then d0=d3 : i=3 If color0>color1 And Abs(color2-color3)<8 If i=3 Then i=2 ;Close and wrong order so use color2 EndIf If format=1 And alphaimg<>0 ;DXT1a, using alpha bit If i=3 Then i=2 ;No color3 so use color2 EndIf If x+ix>width-1 Or y+iy>height-1 ;Out of bounds If color0<color1 Then i=0 Else i=1 ;Find lowest color EndIf texel=PeekByte(hdds,offset+(x*bsize)+bindex+4+iy) Or (i Shl ix*2) PokeByte hdds,offset+(x*bsize)+bindex+4+iy,texel ;bTexels[4] Next Next ;Find color in texel block and set each alpha texel If format=1 And alphaimg<>0 ;DXT1a, using alpha bit For iy=0 To 3 For ix=0 To 3 If x+ix<width And y+iy<height ;Not out of bounds argb=ReadPixelFast(ix+x,iy+y,alphabuf) ;Use alpha map color3=3*16 ;Max transparent color If ColorTotal(argb)<color3 ;Set alpha texel texel=PeekByte(hdds,offset+(x*bsize)+bindex+4+iy) Or (3 Shl ix*2) PokeByte hdds,offset+(x*bsize)+bindex+4+iy,texel ;bTexels[4] EndIf EndIf Next Next EndIf Next Next UnlockBuffer(imagebuf) UnlockBuffer(alphabuf) Next ;Attached Surfaces Data, BYTE bData2[] ;Complex EndIf ;Write DDS bank to file file=WriteFile(filename$) If Not file FreeBank hdds : Return False ;Fail code WriteBytes hdds,file,0,128+sizebytes CloseFile file FreeBank hdds ;If an alpha image was created If alphaimg_created=True Then FreeImage alphaimg Return True ;Success code End Function Function MakeEmptyAlpha(width,height) Local alpha,x,y,rgb alpha=CreateImage(width,height) LockBuffer(ImageBuffer(alpha)) For y=0 To ImageHeight(alpha)-1 For x=0 To ImageWidth(alpha)-1 rgb=$FFFFFF WritePixel x,y,rgb,ImageBuffer(alpha) Next Next UnlockBuffer(ImageBuffer(alpha)) Return alpha End Function Function MAX(a,b) If a=>b Then Return a Else Return b End Function Function MakeFourCC(c0$,c1$,c2$,c3$) Return (Asc(c0$)+(Asc(c1$) Shl 8)+(Asc(c2$) Shl 16)+(Asc(c3$) Shl 24)) End Function Function ColorHighest(argb) Local r,g,b,a r=(argb And $00FF0000) Shr 16 g=(argb And $0000FF00) Shr 8 b=(argb And $000000FF) If r>g Then a=r Else a=g If b>a Then a=b Return a End Function Function ColorTotal(argb) Local r,g,b r=(argb And $00FF0000) Shr 16 g=(argb And $0000FF00) Shr 8 b=(argb And $000000FF) Return (r+g+b) ;0..255*3 End Function Function Color565(argb) Local r,g,b r=(argb And $00FF0000) Shr 16 : r=(r*31/255) Shl 11 ;Bits 11..15 g=(argb And $0000FF00) Shr 8 : g=(g*63/255) Shl 5 ;Bits 5..10 b=(argb And $000000FF) : b=b*31/255 ;Bits 0..4 Return (r+g+b) End Function |
Comments
| ||
Sweet! |
| ||
I converted the code to BlitzMax. There seems to be a little color inaccuracy, compared to DDS files saved by ATI's Compressonator tool. That tool includes some color weighting values that make no sense to me. I think it must be part of a high-pass filter, but their exported DDS files more closely match the original image's colors: |
| ||
So the code works but the colors are a little off? I think that is due to my algorithm, so the Blitz3D version should do exactly the same thing. I wrote this code by reading the DX7 Programmer's Reference, there was no info about what algorithm to use so I came up with my own. If you can find the correct algorithm that ATI use then we could improve it, otherwise I have no idea what their algorithm is. |
| ||
If I set the color weights in ATI's Compressonator to 0 the output looks just like yours. So I think that is what makes the difference. The default values are this: Red: 0.3086 Green: 0.6094 Blue: 0.082 I have no idea how these are being used. They looks like values to convert RGB to grayscale. |
| ||
Ha! It was just a rounding problem. Your code is also about ten times faster than nvdxt.Function Color565:Int(argb) Local r,g,b r=(argb & $00FF0000) Shr 16 ; r=Int((r*31.0/255.0+0.5)) Shl 11 'Bits 11..15 g=(argb & $0000FF00) Shr 8 ; g=Int((g*63.0/255.0+0.5)) Shl 5 'Bits 5..10 b=(argb & $000000FF) ; b=Int(b*31/255.0+0.5) 'Bits 0..4 Return (r+g+b) EndFunction The output is slightly grainy compared to NVidia and ATI tools, but it is hardly different. |
| ||
You fixed it? Great job! I am relieved. :) I had a look at color weighting and that seems to be the process of choosing the most used color in a block of pixels. I'm not sure but I think maybe Color565() should be dividing by 256. Yeah, the grainyness is probably due to the algorithm used. |
| ||
I found it was necessary to clear the created bank, or errors would result after multiple saves. |
| ||
There is an error in your calculation here, at least with BlitzMax integer handling: mipmaps=Log(c)/Log(2)+1 It should be this: mipmaps=Int(Log(c)/Log(2)+1.0+0.5) The wrong mipmap count will be calculated with a texture of size 64, without this fix. |
| ||
Doesn't work in Blitz3D. The problem is that Int() behaves differently in BMax to B3D, in Max it floors the value whereas in B3D it rounds it to the nearest int. I'd actually consider this a bug in Max, but you could say it is a matter of preference. Edit: well looking at it again, Int() in Max is a type cast so it's not a bug but there's no function to do the equivalent, such as Round(). |
| ||
Edit: well looking at it again, Int() in Max is a type cast so it's not a bug but there's no function to do the equivalent, such as Round(). Floor()? |
| ||
No, Int() in b3d isn't like Floor(). With floor (or int type cast in max) 2.99=2, with int in b3d (or round in max) 2.99=3. Edit: in fact, Leadwerks' MathEx module has a Round(). |
| ||
Round%(fFoat#)=Abs(Floor#(Float#+(Sgn(fFloat#))*0.5))) Woudn't this do the trick? |
Code Archives Forum