libdgf/LibDgf.Graphics/Mesh/ObjConverter.cs
2021-03-17 12:55:52 -06:00

420 lines
18 KiB
C#

using LibDgf.Dat;
using LibDgf.Mesh;
using LibDgf.Ps2.Gs;
using LibDgf.Ps2.Gs.Registers;
using LibDgf.Txm;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LibDgf.Graphics.Mesh
{
public class ObjConverter : IDisposable
{
Dictionary<ulong, TxmHeader> textureCache = new Dictionary<ulong, TxmHeader>();
DatReader textureDat;
private bool disposedValue;
int numWrittenTextures = 0;
public ObjConverter(DatReader textureDat)
{
this.textureDat = textureDat;
}
public void ConvertObj(Pdb pdb, StreamWriter sw)
{
int startVert = 1;
if (pdb.Specular != null)
{
sw.WriteLine("o specular");
startVert = WriteObj(pdb.Specular, sw, startVert);
}
if (pdb.Diffuse != null)
{
sw.WriteLine("o diffuse");
startVert = WriteObj(pdb.Diffuse, sw, startVert);
}
if (pdb.Metallic != null)
{
sw.WriteLine("o metallic");
startVert = WriteObj(pdb.Metallic, sw, startVert);
}
}
public void ConvertObj(Tdb tdb, StreamWriter sw)
{
WriteObj(tdb, sw, 1);
}
TxmHeader CreateTexture(Tdb tdb, int index, out ulong textureId)
{
var tdbTexture = tdb.Textures[index];
textureId = ((ulong)tdbTexture.DatIndex << 32) | ((ulong)tdbTexture.ImageBufferBase << 16) | tdbTexture.ClutBufferBase;
if (!textureCache.ContainsKey(textureId))
{
TxmHeader txm = new TxmHeader();
txm.ImageBufferBase = tdbTexture.ImageBufferBase;
txm.ClutBufferBase = tdbTexture.ClutBufferBase;
textureCache.Add(textureId, txm);
return txm;
}
return null;
}
void FillTxm(Db2Element elem, TxmHeader txm)
{
Tex0 tex0 = new Tex0 { Packed = BitConverter.ToUInt64(elem.GsRegs, 0x10) };
txm.ImageSourcePixelFormat = (TxmPixelFormat)tex0.Psm;
txm.ImageVideoPixelFormat = txm.ImageSourcePixelFormat;
txm.ImageWidth = (short)(1 << tex0.Tw);
txm.ImageHeight = (short)(1 << tex0.Th);
txm.Misc = tex0.Tbw;
if (txm.ImageSourcePixelFormat == TxmPixelFormat.PSMT4 || txm.ImageSourcePixelFormat == TxmPixelFormat.PSMT8)
{
txm.ClutPixelFormat = (TxmPixelFormat)tex0.Cpsm;
if (txm.ImageSourcePixelFormat == TxmPixelFormat.PSMT4)
{
txm.ClutWidth = 8;
txm.ClutHeight = 2;
}
else
{
txm.ClutWidth = 16;
txm.ClutHeight = 16;
}
}
else
{
txm.ClutPixelFormat = TxmPixelFormat.None;
}
}
int WriteObj(Tdb tdb, StreamWriter sw, int startVert)
{
for (int i = 0; i < tdb.Mesh.Elements.Count; ++i)
{
var elem = tdb.Mesh.Elements[i];
sw.WriteLine($"g elem_{i}");
TxmHeader txm = CreateTexture(tdb, elem.TextureIndex, out var textureId);
sw.WriteLine($"usemtl tex_{textureId:x12}");
if (txm != null) FillTxm(elem, txm);
// Write vertices
foreach (var vert in elem.Vertices)
{
sw.WriteLine($"v {(double)vert.X} {(double)vert.Y} {(double)vert.Z}");
}
foreach (var norm in elem.VertexNormals)
{
sw.WriteLine($"vn {norm.Item1} {norm.Item2} {norm.Item3}");
}
foreach (var uv in elem.STCoordinates)
{
sw.WriteLine($"vt {uv.Item1} {1 - uv.Item2}");
}
// Write faces
int[] initVerts = new int[2];
bool clockwise = true;
if (elem.GifTagIndex == 3) // Triangle fans
{
int initVertPos = 0;
for (int j = 0; j < elem.Vertices.Count; ++j)
{
if ((elem.Vertices[j].W.Packed & 0x00008000) != 0)
{
initVerts[initVertPos++] = startVert + j;
}
else
{
int currVert = startVert + j;
if (clockwise)
{
sw.WriteLine($"f {initVerts[0]}/{initVerts[0]}/{initVerts[0]} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {currVert}/{currVert}/{currVert}");
}
else
{
sw.WriteLine($"f {currVert}/{currVert}/{currVert} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {initVerts[0]}/{initVerts[0]}/{initVerts[0]}");
}
initVerts[1] = currVert;
initVertPos = 0;
}
clockwise = !clockwise;
}
}
else if (elem.GifTagIndex == 4) // Triangle strips
{
for (int j = 0; j < elem.Vertices.Count; ++j)
{
int currVert = startVert + j;
if ((elem.Vertices[j].W.Packed & 0x00008000) == 0)
{
if (clockwise)
{
sw.WriteLine($"f {initVerts[0]}/{initVerts[0]}/{initVerts[0]} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {currVert}/{currVert}/{currVert}");
}
else
{
sw.WriteLine($"f {currVert}/{currVert}/{currVert} {initVerts[1]}/{initVerts[1]}/{initVerts[1]} {initVerts[0]}/{initVerts[0]}/{initVerts[0]}");
}
}
initVerts[0] = initVerts[1];
initVerts[1] = currVert;
clockwise = !clockwise;
}
}
else
{
throw new NotSupportedException("Unknown face construction type");
}
startVert += elem.Vertices.Count;
sw.WriteLine();
}
return startVert;
}
void CopyTexelsClut(BinaryReader br, BinaryWriter bw, TxmHeader pakTxm, TxmHeader textureTxm)
{
if (pakTxm.ClutPixelFormat != TxmPixelFormat.None)
throw new ArgumentException("Cannot operate on source TXM with CLUT.", nameof(pakTxm));
if (textureTxm.ClutPixelFormat == TxmPixelFormat.None) return;
var destColumnParams = GsMemoryUtils.GetColumnParams(textureTxm.ClutPixelFormat);
int copyLength = textureTxm.GetClutByteSize();
int baseBlockNumber = textureTxm.ClutBufferBase - pakTxm.ImageBufferBase;
int srcBase = 0x10 + pakTxm.GetClutByteSize();
var destBase = 0x10;
int bytesPerSrcLine = pakTxm.GetImageByteSize() / pakTxm.ImageHeight;
int bytesPerDestLine = textureTxm.GetClutByteSize() / textureTxm.ClutHeight;
bw.Write(new byte[copyLength]);
int numXBlocks = textureTxm.ClutWidth / destColumnParams.Width;
if (numXBlocks == 0) numXBlocks = 1;
int numYBlocks = textureTxm.ClutHeight / (destColumnParams.Height * GsMemoryUtils.COLUMNS_PER_BLOCK);
if (numYBlocks == 0) numYBlocks = 1;
int destBlock = 0;
for (int blockY = 0; blockY < numYBlocks; ++blockY)
{
for (int blockX = 0; blockX < numXBlocks; ++blockX)
{
int blockNumber = baseBlockNumber + GsMemoryUtils.CalcBlockNumber(textureTxm.ClutPixelFormat, blockX, blockY, 1);
br.BaseStream.Seek(srcBase + GsMemoryUtils.CalcBlockMemoryOffset(pakTxm.ImageSourcePixelFormat, blockNumber),
SeekOrigin.Begin);
bw.BaseStream.Seek(destBase + GsMemoryUtils.CalcTxmImageOffset(destColumnParams, destBlock, textureTxm.ClutWidth),
SeekOrigin.Begin);
for (int i = 0; i < GsMemoryUtils.COLUMNS_PER_BLOCK; ++i)
{
byte[] col = GsMemoryUtils.ReadColumn(br, pakTxm.ImageSourcePixelFormat, bytesPerSrcLine);
GsMemoryUtils.WriteColumn(bw, textureTxm.ClutPixelFormat, bytesPerDestLine, col);
}
++destBlock;
}
}
// Dump palette
//bw.BaseStream.Seek(destBase, SeekOrigin.Begin);
//BinaryReader palBr = new BinaryReader(bw.BaseStream);
//using (var palette = TxmConversion.ConvertTxmRgba32(palBr, textureTxm.ClutWidth, textureTxm.ClutHeight))
//{
// palette.SaveAsPng($"palette_{numWrittenTextures}.png");
//}
}
void CopyTexels(BinaryReader br, BinaryWriter bw, TxmHeader pakTxm, TxmHeader textureTxm)
{
if (pakTxm.ClutPixelFormat != TxmPixelFormat.None)
throw new ArgumentException("Cannot operate on source TXM with CLUT.", nameof(pakTxm));
var destColumnParams = GsMemoryUtils.GetColumnParams(textureTxm.ImageSourcePixelFormat);
int copyLength = textureTxm.GetImageByteSize();
int srcBase = 0x10 + pakTxm.GetClutByteSize();
int baseBlockNumber = textureTxm.ImageBufferBase - pakTxm.ImageBufferBase;
int destBase = 0x10 + textureTxm.GetClutByteSize();
int bytesPerSrcLine = pakTxm.GetImageByteSize() / pakTxm.ImageHeight;
int bytesPerDestLine = copyLength / textureTxm.ImageHeight;
bw.Write(new byte[copyLength]);
int numXBlocks = textureTxm.ImageWidth / destColumnParams.Width;
if (numXBlocks == 0) numXBlocks = 1;
int numYBlocks = textureTxm.ImageHeight / (destColumnParams.Height * GsMemoryUtils.COLUMNS_PER_BLOCK);
if (numYBlocks == 0) numYBlocks = 1;
int destBlock = 0;
for (int blockY = 0; blockY < numYBlocks; ++blockY)
{
for (int blockX = 0; blockX < numXBlocks; ++blockX)
{
int blockNumber = baseBlockNumber + GsMemoryUtils.CalcBlockNumber(textureTxm.ImageSourcePixelFormat, blockX, blockY, textureTxm.Misc);
br.BaseStream.Seek(srcBase + GsMemoryUtils.CalcBlockMemoryOffset(pakTxm.ImageSourcePixelFormat, blockNumber),
SeekOrigin.Begin);
bw.BaseStream.Seek(destBase + GsMemoryUtils.CalcTxmImageOffset(destColumnParams, destBlock, textureTxm.ImageWidth),
SeekOrigin.Begin);
for (int i = 0; i < GsMemoryUtils.COLUMNS_PER_BLOCK; ++i)
{
byte[] col = GsMemoryUtils.ReadColumn(br, pakTxm.ImageSourcePixelFormat, bytesPerSrcLine);
if (pakTxm.ImageSourcePixelFormat != textureTxm.ImageSourcePixelFormat)
{
col = PsmtMixer.MixColumn(col, pakTxm.ImageSourcePixelFormat, textureTxm.ImageSourcePixelFormat, i % 2 != 0);
}
GsMemoryUtils.WriteColumn(bw, textureTxm.ImageSourcePixelFormat, bytesPerDestLine, col);
}
++destBlock;
}
}
}
public void ExportTextures(StreamWriter mtlWriter, string outputPath, bool forceDirect = false)
{
if (disposedValue) throw new ObjectDisposedException(GetType().FullName);
if (textureDat == null) throw new InvalidOperationException("No texture pack supplied.");
int i = 0;
numWrittenTextures = 0;
foreach (var pair in textureCache.OrderBy(p => p.Key))
{
string pngPath = $"{outputPath}{i}.png";
string alphaPath = $"{outputPath}{i}_alpha.png";
TxmHeader textureTxm = pair.Value;
int txmIndex = (int)(pair.Key >> 32);
using (var txmMs = new MemoryStream(textureDat.GetData(txmIndex)))
{
BinaryReader txmBr = new BinaryReader(txmMs);
TxmHeader pakTxm = new TxmHeader();
pakTxm.Read(txmBr);
Image<Rgba32> img = null;
try
{
// Check if TXM is already suitable
if (forceDirect ||
/*pakTxm.ImageSourcePixelFormat == textureTxm.ImageSourcePixelFormat &&*/
pakTxm.ImageBufferBase == textureTxm.ImageBufferBase &&
pakTxm.ClutPixelFormat == textureTxm.ClutPixelFormat &&
pakTxm.ClutBufferBase == textureTxm.ClutBufferBase)
{
// Use TXM as-is
txmMs.Seek(0, SeekOrigin.Begin);
if (new string(txmBr.ReadChars(4)) == "DAT\0")
{
// Unwrap DAT
txmMs.Seek(0, SeekOrigin.Begin);
using (DatReader txmDat = new DatReader(txmMs))
{
if (txmDat.EntriesCount != 1)
throw new InvalidDataException("Nested texture DAT contains more than one file.");
using (MemoryStream innerStream = new MemoryStream(txmDat.GetData(0)))
{
img = TxmConversion.ConvertTxmToImage(innerStream);
}
}
}
else
{
txmMs.Seek(0, SeekOrigin.Begin);
img = TxmConversion.ConvertTxmToImage(txmMs);
// Dump palette
//if (pakTxm.ClutPixelFormat != TxmPixelFormat.None)
//{
// txmMs.Seek(0x10, SeekOrigin.Begin);
// using (var palette = TxmConversion.ConvertTxmRgba32(txmBr, pakTxm.ClutWidth, pakTxm.ClutHeight))
// {
// palette.SaveAsPng($"palette_{numWrittenTextures}.png");
// }
//}
}
}
else
{
// Generate new TXM
using (MemoryStream ms = new MemoryStream())
{
BinaryWriter bw = new BinaryWriter(ms);
textureTxm.Write(bw);
CopyTexelsClut(txmBr, bw, pakTxm, textureTxm);
CopyTexels(txmBr, bw, pakTxm, textureTxm);
bw.Flush();
ms.Seek(0, SeekOrigin.Begin);
img = TxmConversion.ConvertTxmToImage(ms);
}
}
// Save out color texture
using (var img24bpp = img.CloneAs<Rgb24>())
{
img24bpp.SaveAsPng(pngPath);
}
// Extract alpha channel as a separate image
using (var alphaImg = new Image<L8>(img.Width, img.Height))
{
for (int y = 0; y < alphaImg.Height; ++y)
{
var srcSpan = img.GetPixelRowSpan(y);
var destSpan = alphaImg.GetPixelRowSpan(y);
for (int x = 0; x < alphaImg.Width; ++x)
{
var srcAlpha = srcSpan[x].A;
destSpan[x] = new L8(srcAlpha);
}
}
alphaImg.SaveAsPng(alphaPath);
}
}
finally
{
if (img != null) img.Dispose();
}
}
mtlWriter.WriteLine($"newmtl tex_{pair.Key:x12}");
mtlWriter.WriteLine("Kd 0.80000000 0.80000000 0.80000000");
mtlWriter.WriteLine("Ka 0 0 0");
mtlWriter.WriteLine("Ke 0 0 0");
mtlWriter.WriteLine("Ks 0 0 0");
mtlWriter.WriteLine("d 1");
mtlWriter.WriteLine("illum 2");
mtlWriter.WriteLine($"map_Kd {Path.GetFileName(pngPath)}");
mtlWriter.WriteLine($"map_d {Path.GetFileName(alphaPath)}");
mtlWriter.WriteLine();
++i;
++numWrittenTextures;
}
}
public void Reset()
{
textureCache.Clear();
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (textureDat != null) textureDat.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}