Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ public override void Register(IServiceCollection services)
services.AddTransient<IExporterViewModel, DdsToMaterialPngViewModel>();
services.AddTransient<IExporterViewModel, DdsToNormalPngViewModel>();
services.AddTransient<IExporterViewModel, RmvToGltfExporterViewModel>();
services.AddTransient<IExporterViewModel, RmvToGltfStaticExporterViewModel>();

// Exporters
services.AddTransient<IDdsToMaterialPngExporter, DdsToMaterialPngExporter>();
services.AddTransient<DdsToPngExporter>();
services.AddTransient<IDdsToNormalPngExporter, DdsToNormalPngExporter>();
services.AddTransient<RmvToGltfExporter>();
services.AddTransient<RmvToGltfStaticExporter>();

// Importer ViewModels
RegisterWindow<ImportWindow>(services);
Expand All @@ -60,6 +62,7 @@ public override void Register(IServiceCollection services)
services.AddTransient<DisplayImportFileToolCommand>();

services.AddTransient<GltfMeshBuilder>();
services.AddTransient<GltfStaticMeshBuilder>();
services.AddTransient<IGltfTextureHandler, GltfTextureHandler>();
services.AddTransient<IGltfSceneSaver, GltfSceneSaver>();
services.AddTransient<IGltfSceneLoader, GltfSceneLoader>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.6" />
</ItemGroup>

<ItemGroup>
<Folder Include="Exporting\Exporters\RmvToObj\" />
<Folder Include="Exporting\Presentation\DdsToDisplacementMap\" />
<Folder Include="Exporting\Presentation\RmvToObj\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public string Export(string filePath, string outputPath, bool convertToBlueNorma

var fileName = Path.GetFileNameWithoutExtension(filePath);
var outDirectory = Path.GetDirectoryName(outputPath);
var fullFilePath = outDirectory + "/" + fileName + ".png";
var rawFilePath = outDirectory + "/" + fileName + "_raw.png";

var bytes = packFile.DataSource.ReadData();
if (bytes == null || !bytes.Any())
Expand All @@ -53,73 +53,9 @@ public string Export(string filePath, string outputPath, bool convertToBlueNorma
if (imgBytes == null || !imgBytes.Any())
throw new Exception($"image data invalid/empty. imgBytes.Count = {imgBytes?.Length}");

if (convertToBlueNormalMap)
imgBytes = ConvertToBlueNormalMap(imgBytes, fullFilePath);
_imageSaveHandler.Save(imgBytes, rawFilePath);

_imageSaveHandler.Save(imgBytes, fullFilePath);
return fullFilePath;
return rawFilePath;
}


private byte[] ConvertToBlueNormalMap(byte[] imgBytes, string fileDirectory)
{
var inMs = new MemoryStream(imgBytes);
using Image inImg = Image.FromStream(inMs);

using Bitmap bitmap = new Bitmap(inImg);
{
for (int x = 0; x < bitmap.Width; x++)
{
for (int y = 0; y < bitmap.Height; y++)
{
// get pixel from orange map
var orangeMapRawPixel = bitmap.GetPixel(x, y);

// convert bytes to float to interval [0; 1]
Vector4 orangeMapVector = new Vector4()
{
X = (float)orangeMapRawPixel.R / 255.0f,
Y = (float)orangeMapRawPixel.G / 255.0f,
Z = (float)orangeMapRawPixel.B / 255.0f,
W = (float)orangeMapRawPixel.A / 255.0f,
};

// fill blue map pixels
Vector3 blueMapPixel = new Vector3()
{
X = orangeMapVector.X * orangeMapVector.W,
Y = orangeMapVector.Y,
Z = 0
};

// scale bluemap into interval [-1; 1]
blueMapPixel *= 2.0f;
blueMapPixel -= new Vector3(1, 1, 1);


// calculte z, using an orthogonal projection
blueMapPixel.Z = (float)Math.Sqrt(1.0f - blueMapPixel.X * blueMapPixel.X - blueMapPixel.Y * blueMapPixel.Y);


// convert the float values back to bytes, interval [0; 255]
var newColor = Color.FromArgb(
255,
(byte)((blueMapPixel.X + 1.0f) * 0.5f * 255.0f),
(byte)((blueMapPixel.Y + 1.0f) * 0.5f * 255.0f),
(byte)((blueMapPixel.Z + 1.0f) * 0.5f * 255.0f)
);

bitmap.SetPixel(x, y, newColor);
}
}

// get raw PNG bytes
using var b = new MemoryStream();
bitmap.Save(b, System.Drawing.Imaging.ImageFormat.Png);

return b.ToArray();
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Pfim;

namespace Editors.ImportExport.Exporting.Exporters.RmvToGltf.Helpers
{
/// <summary>
/// Combines a mask texture with a diffuse texture to create a single RGBA texture
/// where the mask becomes the alpha channel.
/// </summary>
public class AlphaMaskCombiner
{
/// <summary>
/// Combines a diffuse (RGB) and mask (Grayscale) into a single RGBA texture.
/// </summary>
public static byte[] CombineDiffuseWithMask(byte[] diffuseDdsBytes, byte[] maskDdsBytes)
{
try
{
// Convert both DDS to bitmaps
var diffuseBitmap = ConvertDdsToBitmap(diffuseDdsBytes);
var maskBitmap = ConvertDdsToBitmap(maskDdsBytes);

// Ensure same dimensions
if (diffuseBitmap.Width != maskBitmap.Width || diffuseBitmap.Height != maskBitmap.Height)
{
throw new InvalidOperationException(
$"Diffuse and mask textures have different dimensions: " +
$"diffuse {diffuseBitmap.Width}x{diffuseBitmap.Height}, " +
$"mask {maskBitmap.Width}x{maskBitmap.Height}");
}

// Create RGBA bitmap
using var combinedBitmap = new Bitmap(diffuseBitmap.Width, diffuseBitmap.Height, PixelFormat.Format32bppArgb);

// Combine pixels
for (int x = 0; x < diffuseBitmap.Width; x++)
{
for (int y = 0; y < diffuseBitmap.Height; y++)
{
var diffusePixel = diffuseBitmap.GetPixel(x, y);
var maskPixel = maskBitmap.GetPixel(x, y);

// Use mask's grayscale value as alpha
int alpha = maskPixel.R; // Grayscale uses same value for R, G, B
var combinedPixel = Color.FromArgb(alpha, diffusePixel.R, diffusePixel.G, diffusePixel.B);

combinedBitmap.SetPixel(x, y, combinedPixel);
}
}

diffuseBitmap.Dispose();
maskBitmap.Dispose();

// Export to PNG with alpha
using var pngStream = new MemoryStream();
combinedBitmap.Save(pngStream, System.Drawing.Imaging.ImageFormat.Png);
return pngStream.ToArray();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to combine diffuse and mask textures: {ex.Message}", ex);
}
}

private static Bitmap ConvertDdsToBitmap(byte[] ddsBytes)
{
using var m = new MemoryStream();
using var w = new BinaryWriter(m);
w.Write(ddsBytes);
m.Seek(0, SeekOrigin.Begin);

var image = Pfimage.FromStream(m);

// Pfim returns BGRA data for Rgba32, but Bitmap expects ARGB
// We need to swap the R and B channels
byte[] correctedData = new byte[image.DataLen];

if (image.Format == Pfim.ImageFormat.Rgba32)
{
// BGRA -> ARGB conversion
for (int i = 0; i < image.DataLen; i += 4)
{
correctedData[i] = image.Data[i + 2]; // B -> R
correctedData[i + 1] = image.Data[i + 1]; // G -> G
correctedData[i + 2] = image.Data[i]; // R -> B
correctedData[i + 3] = image.Data[i + 3]; // A -> A
}
}
else if (image.Format == Pfim.ImageFormat.Rgb24)
{
// BGR -> RGB conversion
for (int i = 0; i < image.DataLen; i += 3)
{
correctedData[i] = image.Data[i + 2]; // B -> R
correctedData[i + 1] = image.Data[i + 1]; // G -> G
correctedData[i + 2] = image.Data[i]; // R -> B
}
}
else
{
// For other formats, use the data as-is
correctedData = image.Data;
}

PixelFormat pixelFormat = image.Format == Pfim.ImageFormat.Rgba32
? PixelFormat.Format32bppArgb
: PixelFormat.Format24bppRgb;

var bitmap = new Bitmap(image.Width, image.Height, pixelFormat);
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.WriteOnly, pixelFormat);
System.Runtime.InteropServices.Marshal.Copy(correctedData, 0, bitmapData.Scan0, correctedData.Length);
bitmap.UnlockBits(bitmapData);

return bitmap;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ namespace Editors.ImportExport.Exporting.Exporters.RmvToGltf.Helpers
{
public class GltfMeshBuilder
{
public List<IMeshBuilder<MaterialBuilder>> Build(RmvFile rmv2, List<TextureResult> textures, RmvToGltfExporterSettings settings)
public List<IMeshBuilder<MaterialBuilder>> Build(RmvFile rmv2, List<TextureResult> textures, RmvToGltfExporterSettings settings, bool willHaveSkeleton = true)
{
var lodLevel = rmv2.ModelList.First();
var hasSkeleton = string.IsNullOrWhiteSpace(rmv2.Header.SkeletonName) == false;
var hasSkeleton = willHaveSkeleton && string.IsNullOrWhiteSpace(rmv2.Header.SkeletonName) == false;

var meshes = new List<IMeshBuilder<MaterialBuilder>>();
for(var i = 0; i < lodLevel.Length; i++)
Expand All @@ -32,7 +32,9 @@ public List<IMeshBuilder<MaterialBuilder>> Build(RmvFile rmv2, List<TextureResul
MeshBuilder<VertexPositionNormalTangent, VertexTexture1, VertexJoints4> GenerateMesh(RmvMesh rmvMesh, string modelName, MaterialBuilder material, bool hasSkeleton, bool doMirror)
{
var mesh = new MeshBuilder<VertexPositionNormalTangent, VertexTexture1, VertexJoints4>(modelName);
if (hasSkeleton)
// Only enable skinning validation if the model has a skeleton and this mesh actually contains weight data
var hasAnyWeights = rmvMesh.VertexList.Any(v => v.WeightCount > 0);
if (hasSkeleton && hasAnyWeights)
mesh.VertexPreprocessor.SetValidationPreprocessors();

var prim = mesh.UsePrimitive(material);
Expand All @@ -53,12 +55,25 @@ MeshBuilder<VertexPositionNormalTangent, VertexTexture1, VertexJoints4> Generate

if (hasSkeleton)
{
glTfvertex = SetVertexInfluences(vertex, glTfvertex);
if (vertex.WeightCount > 0)
{
glTfvertex = SetVertexInfluences(vertex, glTfvertex);
}
else if (hasAnyWeights)
{
// If some vertices have weights in this mesh we enabled validation.
// Ensure vertices without weights get a default binding so validation passes.
glTfvertex.Skinning.SetBindings((0, 1), (0, 0), (0, 0), (0, 0));
}
}
else
else if (hasAnyWeights)
{
// Model has weight data but no skeleton is available.
// Set default binding to prevent validation errors.
glTfvertex.Skinning.SetBindings((0, 1), (0, 0), (0, 0), (0, 0));
}

// For static meshes or vertices handled above, add the vertex
vertexList.Add(glTfvertex);
}

Expand Down Expand Up @@ -88,28 +103,63 @@ MeshBuilder<VertexPositionNormalTangent, VertexTexture1, VertexJoints4> Generate

VertexBuilder<VertexPositionNormalTangent, VertexTexture1, VertexJoints4> SetVertexInfluences(CommonVertex vertex, VertexBuilder<VertexPositionNormalTangent, VertexTexture1, VertexJoints4> glTfvertex)
{
if (vertex.WeightCount == 2)
// Support 1,2,3,4 weight counts and normalize/handle degenerate cases so SharpGLTF validation won't fail.
var weights = new float[4];
var indices = new int[4];

var count = Math.Clamp(vertex.WeightCount, 0, 4);
for (int i = 0; i < count; ++i)
{
indices[i] = vertex.BoneIndex[i];
weights[i] = vertex.BoneWeight[i];
// guard against negative weights from malformed data
if (weights[i] < 0) weights[i] = 0f;
}

// If there are fewer than 4 influences, remaining indices default to 0 and weights to 0
for (int i = count; i < 4; ++i)
{
var rigging = new (int, float)[2] {
(vertex.BoneIndex[0], vertex.BoneWeight[0]),
(vertex.BoneIndex[1], 1.0f - vertex.BoneWeight[0])
};
indices[i] = 0;
weights[i] = 0f;
}

glTfvertex.Skinning.SetBindings(rigging);
float sum = weights[0] + weights[1] + weights[2] + weights[3];

if (sum <= float.Epsilon)
{
// Degenerate: no meaningful weights. Fall back to binding to the first available bone or to bone 0.
if (count > 0)
{
indices[0] = vertex.BoneIndex[0];
weights[0] = 1f;
weights[1] = weights[2] = weights[3] = 0f;
}
else
{
indices[0] = 0;
weights[0] = 1f;
weights[1] = weights[2] = weights[3] = 0f;
}
}
else if (vertex.WeightCount == 4)
else
{
var rigging = new (int, float)[4] {
(vertex.BoneIndex[0], vertex.BoneWeight[0]),
(vertex.BoneIndex[1], vertex.BoneWeight[1]),
(vertex.BoneIndex[2], vertex.BoneWeight[2]),
(vertex.BoneIndex[3], 1.0f - (vertex.BoneWeight[0] + vertex.BoneWeight[1] + vertex.BoneWeight[2]))
};

glTfvertex.Skinning.SetBindings(rigging);
// Normalize weights so they sum to 1
weights[0] /= sum;
weights[1] /= sum;
weights[2] /= sum;
weights[3] /= sum;
}

var rigging = new (int, float)[4]
{
(indices[0], weights[0]),
(indices[1], weights[1]),
(indices[2], weights[2]),
(indices[3], weights[3])
};

glTfvertex.Skinning.SetBindings(rigging);

return glTfvertex;
}

Expand Down
Loading
Loading