Skip to content

Commit 99c908d

Browse files
committed
finalize for release
1 parent 83aa5a1 commit 99c908d

File tree

6 files changed

+208
-34
lines changed

6 files changed

+208
-34
lines changed

DDXConv.sln.DotSettings.user

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2-
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=C_003A_005CUsers_005CKran_005CRiderProjects_005CDDXConv_005CXCompression_005Cbin_005CDebug_005Cnet9_002E0_005CXCompression_002Edll/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2+
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=C_003A_005CUsers_005CKran_005CRiderProjects_005CDDXConv_005CXCompression_005Cbin_005CDebug_005Cnet9_002E0_005CXCompression_002Edll/@EntryIndexedValue">True</s:Boolean>
3+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACompressionFormat_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F4c6572d5eabb7d45d59159fddc6f1edf8cdc1c05b8b793eacd0a21d1963d4_003FCompressionFormat_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
4+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADdsFile_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd3bd93acdde452d922a24663323fdd5c3674df87485ddc579f1dd815ab77f6_003FDdsFile_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
5+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEncoderOutputOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2379330a365d352cc47529f4bafe4f523edf710fc10abd95de74ccd8122d014_003FEncoderOutputOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

DDXConv/DDXConv.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@
2525
<ProjectReference Include="..\XCompression\XCompression.csproj" />
2626
</ItemGroup>
2727

28+
<ItemGroup>
29+
<PackageReference Include="BCnEncoder.Net.ImageSharp" Version="1.1.2" />
30+
</ItemGroup>
31+
2832
</Project>

DDXConv/DdsPostProcessor.cs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using BCnEncoder.Decoder;
2+
using BCnEncoder.Encoder;
3+
using BCnEncoder.ImageSharp;
4+
using BCnEncoder.Shared;
5+
using BCnEncoder.Shared.ImageFiles;
6+
using SixLabors.ImageSharp;
7+
using SixLabors.ImageSharp.PixelFormats;
8+
9+
namespace DDXConv;
10+
11+
public static class DdsPostProcessor
12+
{
13+
// Load BC5 normal map, process to convert from 2-channel to 3-channel normal map, load BC4 specular map, use as alpha
14+
// Save DXT5 format DDS to output path
15+
// Delete specular map after conversion
16+
public static void MergeNormalSpecularMaps(string bc5Path, string? bc4Path)
17+
{
18+
// Decode inputs to Image<Rgba32> using BCnEncoder's decoder helpers.
19+
var decoder = new BcDecoder();
20+
21+
using var bc5Fs = File.OpenRead(bc5Path);
22+
using Image<Rgba32> normalImage = decoder.DecodeToImageRgba32(bc5Fs);
23+
24+
Image<Rgba32> specImage = new Image<Rgba32>(normalImage.Width, normalImage.Height);
25+
26+
if (bc4Path != null)
27+
{
28+
using var bc4Fs = File.OpenRead(bc4Path);
29+
specImage = decoder.DecodeToImageRgba32(bc4Fs);
30+
bc4Fs.Close();
31+
}
32+
else
33+
{
34+
for (int y = 0; y < normalImage.Height; y++)
35+
{
36+
for (int x = 0; x < normalImage.Width; x++)
37+
{
38+
specImage[x, y] = new Rgba32(255, 255, 255, 255);
39+
}
40+
}
41+
}
42+
43+
if (normalImage.Width != specImage.Width || normalImage.Height != specImage.Height)
44+
throw new InvalidOperationException("Input images must have same dimensions.");
45+
46+
// Create combined image
47+
var combined = new Image<Rgba32>(normalImage.Width, normalImage.Height);
48+
49+
// Iterate pixels.
50+
// Assumes normalImage R,G channels are the signed XY encoded as 0..255 -> -1..1
51+
// We'll reconstruct Z = sqrt(1 - x^2 - y^2) and map to 0..255.
52+
for (int y = 0; y < normalImage.Height; y++)
53+
{
54+
for (int x = 0; x < normalImage.Width; x++)
55+
{
56+
Rgba32 npx = normalImage.Frames[0].PixelBuffer[x,y];
57+
Rgba32 spx = specImage.Frames[0].PixelBuffer[x,y];
58+
59+
// Convert from [0..255] to [-1..1]
60+
float nx = (npx.R / 255f) * 2f - 1f;
61+
float ny = (npx.G / 255f) * 2f - 1f;
62+
63+
// Compute z (clamp small negative to 0)
64+
float nz2 = 1f - nx * nx - ny * ny;
65+
float nz = nz2 > 0f ? (float)Math.Sqrt(nz2) : 0f;
66+
67+
// Remap to [0..255]
68+
byte outR = (byte)MathF.Round((nx * 0.5f + 0.5f) * 255f);
69+
byte outG = (byte)MathF.Round((ny * 0.5f + 0.5f) * 255f);
70+
byte outB = (byte)MathF.Round((nz * 0.5f + 0.5f) * 255f);
71+
72+
// Spec map: use red channel (or luminance). We use red here.
73+
byte outA = spx.R;
74+
75+
combined[x,y] = new Rgba32(outR, outG, outB, outA);
76+
}
77+
}
78+
79+
// Encode to DXT5 / BC3 using BCnEncoder
80+
var encoder = new BcEncoder
81+
{
82+
OutputOptions =
83+
{
84+
GenerateMipMaps = true, // generate full mip chain
85+
Format = CompressionFormat.Bc3,
86+
FileFormat = OutputFileFormat.Dds,
87+
Quality = CompressionQuality.Balanced
88+
}
89+
};
90+
91+
bc5Fs.Close();
92+
using var outFs = File.OpenWrite(bc5Path);
93+
encoder.EncodeToStream(combined, outFs);
94+
outFs.Seek(0x44, SeekOrigin.Begin);
95+
outFs.Write("KRAN"u8);
96+
outFs.Close();
97+
98+
// Delete specular map
99+
if (bc4Path != null) File.Delete(bc4Path);
100+
}
101+
102+
private static CompressionFormat GetCompressionFromPixelFormat(uint pf)
103+
{
104+
if (pf == DdsPixelFormat.Dxt1)
105+
return CompressionFormat.Bc1;
106+
if (pf == DdsPixelFormat.Dxt3)
107+
return CompressionFormat.Bc2;
108+
if (pf == DdsPixelFormat.Dxt5)
109+
return CompressionFormat.Bc3;
110+
if (pf == DdsPixelFormat.Ati1)
111+
return CompressionFormat.Bc4;
112+
if (pf == DdsPixelFormat.Ati2)
113+
return CompressionFormat.Bc5;
114+
throw new NotSupportedException("Unsupported pixel format: " + pf);
115+
}
116+
117+
public static void RegenerateMips(string ddsPath)
118+
{
119+
var decoder = new BcDecoder();
120+
using var fs = File.OpenRead(ddsPath);
121+
var dds = DdsFile.Load(fs);
122+
using Image<Rgba32> image = decoder.DecodeToImageRgba32(dds);
123+
fs.Close();
124+
125+
var encoder = new BcEncoder
126+
{
127+
OutputOptions =
128+
{
129+
GenerateMipMaps = dds.header.dwMipMapCount > 1,
130+
Format = GetCompressionFromPixelFormat(dds.header.ddsPixelFormat.dwFourCc),
131+
FileFormat = OutputFileFormat.Dds,
132+
Quality = CompressionQuality.BestQuality
133+
}
134+
};
135+
136+
// Encode to a temporary file then replace the original to avoid corrupting the file on error.
137+
var tmpPath = ddsPath + ".regen.tmp";
138+
using (var outFs = File.Create(tmpPath))
139+
{
140+
encoder.EncodeToStream(image, outFs);
141+
outFs.Seek(0x44, SeekOrigin.Begin);
142+
outFs.Write("KRAN"u8);
143+
}
144+
145+
File.Delete(ddsPath);
146+
File.Move(tmpPath, ddsPath);
147+
}
148+
}

DDXConv/DdxParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace DDXConv;
55
public class DdxParser
66
{
77
private readonly bool verboseLogging;
8+
89
public DdxParser(bool verbose = false)
910
{
1011
verboseLogging = verbose;

DDXConv/Program.cs

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
namespace DDXConv;
1+
using System;
2+
using System.IO;
23

4+
namespace DDXConv;
35
internal class Program
46
{
57
private static void Main(string[] args)
@@ -20,12 +22,11 @@ private static void Main(string[] args)
2022
{
2123
Console.WriteLine("Single File: DDXConv <input_file> [output_file] [options]");
2224
Console.WriteLine(" Batch: DDXConv <input_directory> <output_directory> [options]");
23-
Console.WriteLine("Converts Xbox 360 DDX texture files to DDS format");
2425
Console.WriteLine("Standard Options:");
2526
Console.WriteLine(" --pc-friendly, -pc Produce PC-ready normal maps (batch conversion only!)");
2627
Console.WriteLine(" --regen-mips, -g Regenerate mip levels from top level");
2728
Console.WriteLine("Developer Options:");
28-
Console.WriteLine(" --atlas, -a Save untiled atlas as separate DDS file");
29+
Console.WriteLine(" --atlas, -a Save untiled mip atlas as separate DDS file");
2930
Console.WriteLine(" --raw, -r Save raw combined decompressed data as binary file");
3031
Console.WriteLine(" --save-mips Save extracted mip levels from atlas");
3132
Console.WriteLine(" --no-untile-atlas Do not untile/unswizzle the atlas (leave tiled)");
@@ -70,16 +71,17 @@ private static void Main(string[] args)
7071

7172
try
7273
{
73-
var parser = new DdxParser();
74+
var parser = new DdxParser(verbose);
7475
parser.ConvertDdxToDds(ddxFile, outputBatchPath,
7576
new ConversionOptions
7677
{
7778
SaveAtlas = saveAtlas, SaveRaw = saveRaw, SaveMips = saveMips,
7879
NoUntileAtlas = noUntileAtlas, SkipEndianSwap = skipEndianSwap
7980
});
8081
Console.WriteLine($"Converted {ddxFile} to {outputBatchPath}");
82+
if (regenMips) DdsPostProcessor.RegenerateMips(outputBatchPath);
8183
}
82-
catch (InvalidDataException ex)
84+
catch (NotSupportedException)
8385
{
8486
invalids++;
8587
}
@@ -92,7 +94,7 @@ private static void Main(string[] args)
9294
}
9395

9496
Console.WriteLine(
95-
$"Batch conversion completed. Successfully converted {ddxFiles.Length - errors - invalids} out of {ddxFiles.Length} files ({errors} failures, {invalids} invalids).");
97+
$"Batch conversion completed. Successfully converted {ddxFiles.Length - errors - invalids} out of {ddxFiles.Length} files ({errors} failures, {invalids} unsupported).");
9698
foreach (var tex in failed) Console.Write($"- {tex.name}: {tex.error}\n");
9799

98100
if (pcFriendly)
@@ -103,16 +105,14 @@ private static void Main(string[] args)
103105
{
104106
// check if file with same name but _s.dds exists
105107
var specFile = ddsFile.Replace("_n.dds", "_s.dds");
106-
if (File.Exists(specFile))
108+
try
107109
{
108-
try
109-
{
110-
Console.WriteLine($"Converted to PC-friendly normal map: {ddsFile}");
111-
}
112-
catch (Exception ex)
113-
{
114-
Console.WriteLine($"Error converting to PC-friendly normal map {ddsFile}: {ex.Message}");
115-
}
110+
DdsPostProcessor.MergeNormalSpecularMaps(ddsFile, File.Exists(specFile) ? specFile : null);
111+
Console.WriteLine($"Converted to PC-friendly normal map: {ddsFile}");
112+
}
113+
catch (Exception ex)
114+
{
115+
Console.WriteLine($"Error converting to PC-friendly normal map {ddsFile}: {ex.Message}");
116116
}
117117
}
118118
}
@@ -138,26 +138,14 @@ private static void Main(string[] args)
138138
SkipEndianSwap = skipEndianSwap
139139
});
140140
Console.WriteLine($"Successfully converted {inputPath} to {outputPath}");
141+
// Regenerate mips unless disabled or if PC-friendly normal map case (no reason to add another re-encode since normal merge regenerates mips)
142+
if (!regenMips || ((inputPath.EndsWith("_s.dds") || inputPath.EndsWith("_n.dds")) && pcFriendly)) return;
143+
DdsPostProcessor.RegenerateMips(outputPath);
141144
}
142145
catch (Exception ex)
143146
{
144147
Console.WriteLine($"Error: {ex.Message}");
145148
Console.WriteLine(ex.StackTrace);
146149
}
147-
}
148-
}
149-
150-
public static class NormalConverter
151-
{
152-
public static void ConvertToPcFriendly(string normalFilePath, string specFilePath)
153-
{
154-
// Load BC5 normal map, convert to 3-channel normal map, load BC4 specular map, use as alpha
155-
// Save DXT5 format DDS to output path
156-
// Delete specular map after conversion
157-
158-
var normalData = File.ReadAllBytes(normalFilePath);
159-
var specData = File.ReadAllBytes(specFilePath);
160-
161-
162150
}
163151
}

README.MD

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
# DDXConv
2-
### Tool to convert New Vegas Xbox 360 .ddx textures to standard .dds
2+
### Tool to convert New Vegas Xbox 360 .ddx textures to standard .dds files usable in the PC game
33

44
---
55

6-
Usage: `DDXConv.exe <input> [output]` or `DDXConv.exe <input folder> <output folder>`
6+
Current status (numbers from July 2010 proto): Converts *most* textures correctly (22148 out of 26123). Generally, if you want a texture out of the game, you can probably get it with this tool. Everything I have wanted has been supported.
7+
8+
Known Issues:
9+
- Some textures appear to have issues with their mipmaps, I have made an effort to extract them properly, but an option to regenerate mips is included (`-g` or `--regen-mips`) so you can reliably get good data.
10+
- 13 3XDO files fail to convert for one reason or another. This seems to be related to them having a low resolution in at least one axis (8px or lower)
11+
- 3XDR format textures are not supported. This affects 3961 textures.
12+
13+
Pull requests and issues are welcome to address these issues or any other bugs you may find.
714

815
---
916

17+
### Usage
18+
You are expected to extract ddx files from bsa archives yourself. Support will not be added for this.
19+
```
20+
Single File: DDXConv <input_file> [output_file] [options]
21+
Batch: DDXConv <input_directory> <output_directory> [options]
22+
Standard Options:
23+
--pc-friendly, -pc Produce PC-ready normal maps (batch conversion only!)
24+
--regen-mips, -g Regenerate mip levels from top level
25+
Developer Options:
26+
--atlas, -a Save untiled mip atlas as separate DDS file
27+
--raw, -r Save raw combined decompressed data as binary file
28+
--save-mips Save extracted mip levels from atlas
29+
--no-untile-atlas Do not untile/unswizzle the atlas (leave tiled)
30+
--no-swap Do not perform endian swap on data
31+
--verbose, -v Enable verbose output
32+
```
33+
1034
Requires [XNA Framework Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=20914) and [.NET 9.0 Runtime (x86)](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) to run.
1135

1236
---
1337

1438
### Credits
1539
Untiling code based on [xenia](https://github.com/xenia-project/xenia)
16-
<br>Decompression handled via [XCompression](https://github.com/gibbed/XCompression)
40+
<br>Decompression handled via [XCompression](https://github.com/gibbed/XCompression) (thanks MrPinball64 for the pointer!)
41+
<br>Special thanks to everyone in the TriangleCity Discord for their accompaniment while I worked on this.
42+
43+
---
44+
45+
### Other Useful Tools for New Vegas Prototype files
46+
- [Save Image Extractor](https://gist.github.com/kran27/291349139c769173a0f02e1eb2462975)

0 commit comments

Comments
 (0)