Remove Utility module and move its content to Core and TextRenderer modules

This commit is contained in:
SirLynix
2024-02-10 22:46:53 +01:00
committed by Jérôme Leclercq
parent 965a00182c
commit e64c2b036e
364 changed files with 2336 additions and 2516 deletions

View File

@@ -0,0 +1,91 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/DDSConstants.hpp>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
bool Unserialize(SerializationContext& context, DDSHeader* header)
{
if (!Unserialize(context, &header->size))
return false;
if (!Unserialize(context, &header->flags))
return false;
if (!Unserialize(context, &header->height))
return false;
if (!Unserialize(context, &header->width))
return false;
if (!Unserialize(context, &header->pitch))
return false;
if (!Unserialize(context, &header->depth))
return false;
if (!Unserialize(context, &header->levelCount))
return false;
for (unsigned int i = 0; i < CountOf(header->reserved1); ++i)
{
if (!Unserialize(context, &header->reserved1[i]))
return false;
}
if (!Unserialize(context, &header->format))
return false;
for (unsigned int i = 0; i < CountOf(header->ddsCaps); ++i)
{
if (!Unserialize(context, &header->ddsCaps[i]))
return false;
}
if (!Unserialize(context, &header->reserved2))
return false;
return true;
}
bool Unserialize(SerializationContext& context, DDSHeaderDX10Ext* header)
{
UInt32 enumValue;
if (!Unserialize(context, &enumValue))
return false;
header->dxgiFormat = static_cast<DXGI_FORMAT>(enumValue);
if (!Unserialize(context, &enumValue))
return false;
header->resourceDimension = static_cast<D3D10_RESOURCE_DIMENSION>(enumValue);
if (!Unserialize(context, &header->miscFlag))
return false;
if (!Unserialize(context, &header->arraySize))
return false;
if (!Unserialize(context, &header->reserved))
return false;
return true;
}
bool Unserialize(SerializationContext& context, DDSPixelFormat* pixelFormat)
{
if (!Unserialize(context, &pixelFormat->size))
return false;
if (!Unserialize(context, &pixelFormat->flags))
return false;
if (!Unserialize(context, &pixelFormat->fourCC))
return false;
if (!Unserialize(context, &pixelFormat->bpp))
return false;
if (!Unserialize(context, &pixelFormat->redMask))
return false;
if (!Unserialize(context, &pixelFormat->greenMask))
return false;
if (!Unserialize(context, &pixelFormat->blueMask))
return false;
if (!Unserialize(context, &pixelFormat->alphaMask))
return false;
return true;
}
}

View File

@@ -0,0 +1,377 @@
// Copyright (C) 2024 Cruden BV - 2020 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_DDSCONSTANTS_HPP
#define NAZARA_CORE_FORMATS_DDSCONSTANTS_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Config.hpp>
#include <Nazara/Core/Serialization.hpp>
namespace Nz
{
inline constexpr UInt32 DDS_FourCC(UInt32 a, UInt32 b, UInt32 c, UInt32 d)
{
return a << 0 |
b << 8 |
c << 16 |
d << 24;
}
constexpr UInt32 DDS_Magic = DDS_FourCC('D', 'D', 'S', ' ');
enum D3D10_RESOURCE_DIMENSION
{
D3D10_RESOURCE_DIMENSION_UNKNOWN = 0,
D3D10_RESOURCE_DIMENSION_BUFFER = 1,
D3D10_RESOURCE_DIMENSION_TEXTURE1D = 2,
D3D10_RESOURCE_DIMENSION_TEXTURE2D = 3,
D3D10_RESOURCE_DIMENSION_TEXTURE3D = 4
};
enum D3D10_RESOURCE_MISC
{
D3D10_RESOURCE_MISC_GENERATE_MIPS = 0x1L,
D3D10_RESOURCE_MISC_SHARED = 0x2L,
D3D10_RESOURCE_MISC_TEXTURECUBE = 0x4L,
D3D10_RESOURCE_MISC_SHARED_KEYEDMUTEX = 0x10L,
D3D10_RESOURCE_MISC_GDI_COMPATIBLE = 0x20L
};
enum D3DFMT
{
D3DFMT_UNKNOWN = 0,
D3DFMT_R8G8B8 = 20,
D3DFMT_A8R8G8B8 = 21,
D3DFMT_X8R8G8B8 = 22,
D3DFMT_R5G6B5 = 23,
D3DFMT_X1R5G5B5 = 24,
D3DFMT_A1R5G5B5 = 25,
D3DFMT_A4R4G4B4 = 26,
D3DFMT_R3G3B2 = 27,
D3DFMT_A8 = 28,
D3DFMT_A8R3G3B2 = 29,
D3DFMT_X4R4G4B4 = 30,
D3DFMT_A2B10G10R10 = 31,
D3DFMT_A8B8G8R8 = 32,
D3DFMT_X8B8G8R8 = 33,
D3DFMT_G16R16 = 34,
D3DFMT_A2R10G10B10 = 35,
D3DFMT_A16B16G16R16 = 36,
D3DFMT_A8P8 = 40,
D3DFMT_P8 = 41,
D3DFMT_L8 = 50,
D3DFMT_A8L8 = 51,
D3DFMT_A4L4 = 52,
D3DFMT_V8U8 = 60,
D3DFMT_L6V5U5 = 61,
D3DFMT_X8L8V8U8 = 62,
D3DFMT_Q8W8V8U8 = 63,
D3DFMT_V16U16 = 64,
D3DFMT_A2W10V10U10 = 67,
D3DFMT_UYVY = DDS_FourCC('U', 'Y', 'V', 'Y'),
D3DFMT_R8G8_B8G8 = DDS_FourCC('R', 'G', 'B', 'G'),
D3DFMT_YUY2 = DDS_FourCC('Y', 'U', 'Y', '2'),
D3DFMT_G8R8_G8B8 = DDS_FourCC('G', 'R', 'G', 'B'),
D3DFMT_DXT1 = DDS_FourCC('D', 'X', 'T', '1'),
D3DFMT_DXT2 = DDS_FourCC('D', 'X', 'T', '2'),
D3DFMT_DXT3 = DDS_FourCC('D', 'X', 'T', '3'),
D3DFMT_DXT4 = DDS_FourCC('D', 'X', 'T', '4'),
D3DFMT_DXT5 = DDS_FourCC('D', 'X', 'T', '5'),
D3DFMT_D16_LOCKABLE = 70,
D3DFMT_D32 = 71,
D3DFMT_D15S1 = 73,
D3DFMT_D24S8 = 75,
D3DFMT_D24X8 = 77,
D3DFMT_D24X4S4 = 79,
D3DFMT_D16 = 80,
D3DFMT_D32F_LOCKABLE = 82,
D3DFMT_D24FS8 = 83,
D3DFMT_L16 = 81,
D3DFMT_VERTEXDATA = 100,
D3DFMT_INDEX16 = 101,
D3DFMT_INDEX32 = 102,
D3DFMT_Q16W16V16U16 = 110,
D3DFMT_MULTI2_ARGB8 = DDS_FourCC('M','E','T','1'),
D3DFMT_R16F = 111,
D3DFMT_G16R16F = 112,
D3DFMT_A16B16G16R16F = 113,
D3DFMT_R32F = 114,
D3DFMT_G32R32F = 115,
D3DFMT_A32B32G32R32F = 116,
D3DFMT_CxV8U8 = 117,
D3DFMT_DX10 = DDS_FourCC('D', 'X', '1', '0')
};
enum DDPF
{
DDPF_ALPHAPIXELS = 0x00001,
DDPF_ALPHA = 0x00002,
DDPF_FOURCC = 0x00004,
DDPF_RGB = 0x00040,
DDPF_YUV = 0x00200,
DDPF_LUMINANCE = 0x20000
};
enum DDSD
{
DDSD_CAPS = 0x00000001,
DDSD_HEIGHT = 0x00000002,
DDSD_WIDTH = 0x00000004,
DDSD_PITCH = 0x00000008,
DDSD_PIXELFORMAT = 0x00001000,
DDSD_MIPMAPCOUNT = 0x00020000,
DDSD_LINEARSIZE = 0x00080000,
DDSD_DEPTH = 0x00800000
};
enum DDSCAPS
{
DDSCAPS_COMPLEX = 0x00000008,
DDSCAPS_MIPMAP = 0x00400000,
DDSCAPS_TEXTURE = 0x00001000
};
enum DDSCAPS2
{
DDSCAPS2_CUBEMAP = 0x00000200,
DDSCAPS2_CUBEMAP_POSITIVEX = 0x00000400,
DDSCAPS2_CUBEMAP_NEGATIVEX = 0x00000800,
DDSCAPS2_CUBEMAP_POSITIVEY = 0x00001000,
DDSCAPS2_CUBEMAP_NEGATIVEY = 0x00002000,
DDSCAPS2_CUBEMAP_POSITIVEZ = 0x00004000,
DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000,
DDSCAPS2_VOLUME = 0x00200000,
DDSCAPS2_CUBEMAP_ALLFACES = DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX |
DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY |
DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ
};
enum DDS_COLOR
{
DDS_COLOR_DEFAULT = 0,
DDS_COLOR_DISTANCE,
DDS_COLOR_LUMINANCE,
DDS_COLOR_INSET_BBOX,
DDS_COLOR_MAX
};
enum DDS_COMPRESS
{
DDS_COMPRESS_NONE = 0,
DDS_COMPRESS_BC1, /* DXT1 */
DDS_COMPRESS_BC2, /* DXT3 */
DDS_COMPRESS_BC3, /* DXT5 */
DDS_COMPRESS_BC3N, /* DXT5n */
DDS_COMPRESS_BC4, /* ATI1 */
DDS_COMPRESS_BC5, /* ATI2 */
DDS_COMPRESS_AEXP, /* DXT5 */
DDS_COMPRESS_YCOCG, /* DXT5 */
DDS_COMPRESS_YCOCGS, /* DXT5 */
DDS_COMPRESS_MAX
};
enum DDS_FORMAT
{
DDS_FORMAT_DEFAULT = 0,
DDS_FORMAT_RGB8,
DDS_FORMAT_RGBA8,
DDS_FORMAT_BGR8,
DDS_FORMAT_ABGR8,
DDS_FORMAT_R5G6B5,
DDS_FORMAT_RGBA4,
DDS_FORMAT_RGB5A1,
DDS_FORMAT_RGB10A2,
DDS_FORMAT_R3G3B2,
DDS_FORMAT_A8,
DDS_FORMAT_L8,
DDS_FORMAT_L8A8,
DDS_FORMAT_AEXP,
DDS_FORMAT_YCOCG,
DDS_FORMAT_MAX
};
enum DDS_MIPMAP
{
DDS_MIPMAP_DEFAULT = 0,
DDS_MIPMAP_NEAREST,
DDS_MIPMAP_BOX,
DDS_MIPMAP_BILINEAR,
DDS_MIPMAP_BICUBIC,
DDS_MIPMAP_LANCZOS,
DDS_MIPMAP_MAX
};
enum DDS_SAVE
{
DDS_SAVE_SELECTED_LAYER = 0,
DDS_SAVE_CUBEMAP,
DDS_SAVE_VOLUMEMAP,
DDS_SAVE_MAX
};
enum DXGI_FORMAT
{
DXGI_FORMAT_UNKNOWN = 0,
DXGI_FORMAT_R32G32B32A32_TYPELESS = 1,
DXGI_FORMAT_R32G32B32A32_FLOAT = 2,
DXGI_FORMAT_R32G32B32A32_UINT = 3,
DXGI_FORMAT_R32G32B32A32_SINT = 4,
DXGI_FORMAT_R32G32B32_TYPELESS = 5,
DXGI_FORMAT_R32G32B32_FLOAT = 6,
DXGI_FORMAT_R32G32B32_UINT = 7,
DXGI_FORMAT_R32G32B32_SINT = 8,
DXGI_FORMAT_R16G16B16A16_TYPELESS = 9,
DXGI_FORMAT_R16G16B16A16_FLOAT = 10,
DXGI_FORMAT_R16G16B16A16_UNORM = 11,
DXGI_FORMAT_R16G16B16A16_UINT = 12,
DXGI_FORMAT_R16G16B16A16_SNORM = 13,
DXGI_FORMAT_R16G16B16A16_SINT = 14,
DXGI_FORMAT_R32G32_TYPELESS = 15,
DXGI_FORMAT_R32G32_FLOAT = 16,
DXGI_FORMAT_R32G32_UINT = 17,
DXGI_FORMAT_R32G32_SINT = 18,
DXGI_FORMAT_R32G8X24_TYPELESS = 19,
DXGI_FORMAT_D32_FLOAT_S8X24_UINT = 20,
DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS = 21,
DXGI_FORMAT_X32_TYPELESS_G8X24_UINT = 22,
DXGI_FORMAT_R10G10B10A2_TYPELESS = 23,
DXGI_FORMAT_R10G10B10A2_UNORM = 24,
DXGI_FORMAT_R10G10B10A2_UINT = 25,
DXGI_FORMAT_R11G11B10_FLOAT = 26,
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27,
DXGI_FORMAT_R8G8B8A8_UNORM = 28,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29,
DXGI_FORMAT_R8G8B8A8_UINT = 30,
DXGI_FORMAT_R8G8B8A8_SNORM = 31,
DXGI_FORMAT_R8G8B8A8_SINT = 32,
DXGI_FORMAT_R16G16_TYPELESS = 33,
DXGI_FORMAT_R16G16_FLOAT = 34,
DXGI_FORMAT_R16G16_UNORM = 35,
DXGI_FORMAT_R16G16_UINT = 36,
DXGI_FORMAT_R16G16_SNORM = 37,
DXGI_FORMAT_R16G16_SINT = 38,
DXGI_FORMAT_R32_TYPELESS = 39,
DXGI_FORMAT_D32_FLOAT = 40,
DXGI_FORMAT_R32_FLOAT = 41,
DXGI_FORMAT_R32_UINT = 42,
DXGI_FORMAT_R32_SINT = 43,
DXGI_FORMAT_R24G8_TYPELESS = 44,
DXGI_FORMAT_D24_UNORM_S8_UINT = 45,
DXGI_FORMAT_R24_UNORM_X8_TYPELESS = 46,
DXGI_FORMAT_X24_TYPELESS_G8_UINT = 47,
DXGI_FORMAT_R8G8_TYPELESS = 48,
DXGI_FORMAT_R8G8_UNORM = 49,
DXGI_FORMAT_R8G8_UINT = 50,
DXGI_FORMAT_R8G8_SNORM = 51,
DXGI_FORMAT_R8G8_SINT = 52,
DXGI_FORMAT_R16_TYPELESS = 53,
DXGI_FORMAT_R16_FLOAT = 54,
DXGI_FORMAT_D16_UNORM = 55,
DXGI_FORMAT_R16_UNORM = 56,
DXGI_FORMAT_R16_UINT = 57,
DXGI_FORMAT_R16_SNORM = 58,
DXGI_FORMAT_R16_SINT = 59,
DXGI_FORMAT_R8_TYPELESS = 60,
DXGI_FORMAT_R8_UNORM = 61,
DXGI_FORMAT_R8_UINT = 62,
DXGI_FORMAT_R8_SNORM = 63,
DXGI_FORMAT_R8_SINT = 64,
DXGI_FORMAT_A8_UNORM = 65,
DXGI_FORMAT_R1_UNORM = 66,
DXGI_FORMAT_R9G9B9E5_SHAREDEXP = 67,
DXGI_FORMAT_R8G8_B8G8_UNORM = 68,
DXGI_FORMAT_G8R8_G8B8_UNORM = 69,
DXGI_FORMAT_BC1_TYPELESS = 70,
DXGI_FORMAT_BC1_UNORM = 71,
DXGI_FORMAT_BC1_UNORM_SRGB = 72,
DXGI_FORMAT_BC2_TYPELESS = 73,
DXGI_FORMAT_BC2_UNORM = 74,
DXGI_FORMAT_BC2_UNORM_SRGB = 75,
DXGI_FORMAT_BC3_TYPELESS = 76,
DXGI_FORMAT_BC3_UNORM = 77,
DXGI_FORMAT_BC3_UNORM_SRGB = 78,
DXGI_FORMAT_BC4_TYPELESS = 79,
DXGI_FORMAT_BC4_UNORM = 80,
DXGI_FORMAT_BC4_SNORM = 81,
DXGI_FORMAT_BC5_TYPELESS = 82,
DXGI_FORMAT_BC5_UNORM = 83,
DXGI_FORMAT_BC5_SNORM = 84,
DXGI_FORMAT_B5G6R5_UNORM = 85,
DXGI_FORMAT_B5G5R5A1_UNORM = 86,
DXGI_FORMAT_B8G8R8A8_UNORM = 87,
DXGI_FORMAT_B8G8R8X8_UNORM = 88,
DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM = 89,
DXGI_FORMAT_B8G8R8A8_TYPELESS = 90,
DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91,
DXGI_FORMAT_B8G8R8X8_TYPELESS = 92,
DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93,
DXGI_FORMAT_BC6H_TYPELESS = 94,
DXGI_FORMAT_BC6H_UF16 = 95,
DXGI_FORMAT_BC6H_SF16 = 96,
DXGI_FORMAT_BC7_TYPELESS = 97,
DXGI_FORMAT_BC7_UNORM = 98,
DXGI_FORMAT_BC7_UNORM_SRGB = 99
};
struct DDSPixelFormat // DDPIXELFORMAT
{
UInt32 size;
UInt32 flags;
UInt32 fourCC;
UInt32 bpp;
UInt32 redMask;
UInt32 greenMask;
UInt32 blueMask;
UInt32 alphaMask;
};
struct DDSHeader
{
UInt32 size;
UInt32 flags;
UInt32 height;
UInt32 width;
UInt32 pitch;
UInt32 depth;
UInt32 levelCount;
UInt32 reserved1[11];
DDSPixelFormat format;
UInt32 ddsCaps[4];
UInt32 reserved2;
};
struct DDSHeaderDX10Ext
{
DXGI_FORMAT dxgiFormat;
D3D10_RESOURCE_DIMENSION resourceDimension;
UInt32 miscFlag;
UInt32 arraySize;
UInt32 reserved;
};
NAZARA_CORE_API bool Unserialize(SerializationContext& context, DDSHeader* header);
NAZARA_CORE_API bool Unserialize(SerializationContext& context, DDSHeaderDX10Ext* header);
NAZARA_CORE_API bool Unserialize(SerializationContext& context, DDSPixelFormat* pixelFormat);
}
#endif // NAZARA_CORE_FORMATS_DDSCONSTANTS_HPP

View File

@@ -0,0 +1,270 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) - 2009 Cruden BV
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/DDSLoader.hpp>
#include <Nazara/Core/ByteStream.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Image.hpp>
#include <Nazara/Core/PixelFormat.hpp>
#include <Nazara/Core/Formats/DDSConstants.hpp>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
class DDSLoader
{
public:
DDSLoader() = delete;
~DDSLoader() = delete;
static bool IsSupported(std::string_view extension)
{
return (extension == ".dds");
}
static Result<std::shared_ptr<Image>, ResourceLoadingError> Load(Stream& stream, const ImageParams& parameters)
{
ByteStream byteStream(&stream);
byteStream.SetDataEndianness(Endianness::LittleEndian);
UInt32 magic;
byteStream >> magic;
if (magic != DDS_Magic)
return Nz::Err(ResourceLoadingError::Unrecognized);
DDSHeader header;
byteStream >> header;
DDSHeaderDX10Ext headerDX10;
if (header.format.flags & DDPF_FOURCC && header.format.fourCC == D3DFMT_DX10)
byteStream >> headerDX10;
else
{
headerDX10.arraySize = 1;
headerDX10.dxgiFormat = DXGI_FORMAT_UNKNOWN;
headerDX10.miscFlag = 0;
headerDX10.resourceDimension = D3D10_RESOURCE_DIMENSION_UNKNOWN;
}
if ((header.flags & DDSD_WIDTH) == 0)
NazaraWarning("Ill-formed DDS file, doesn't have a width flag");
unsigned int width = std::max(header.width, 1U);
unsigned int height = 1U;
if (header.flags & DDSD_HEIGHT)
height = std::max(header.height, 1U);
unsigned int depth = 1U;
if (header.flags & DDSD_DEPTH)
depth = std::max(header.depth, 1U);
UInt8 levelCount = (parameters.levelCount > 0) ? std::min(parameters.levelCount, SafeCast<UInt8>(header.levelCount)) : SafeCast<UInt8>(header.levelCount);
// First, identify the type
ImageType type;
if (!IdentifyImageType(header, headerDX10, &type))
return Nz::Err(ResourceLoadingError::Unsupported);
// Then the format
PixelFormat format;
if (!IdentifyPixelFormat(header, headerDX10, &format))
return Nz::Err(ResourceLoadingError::Unsupported);
std::shared_ptr<Image> image = std::make_shared<Image>(type, format, width, height, depth, levelCount);
// Read all mipmap levels
for (UInt8 i = 0; i < image->GetLevelCount(); i++)
{
std::size_t byteCount = PixelFormatInfo::ComputeSize(format, width, height, depth);
UInt8* ptr = image->GetPixels(0, 0, 0, i);
if (byteStream.Read(ptr, byteCount) != byteCount)
{
NazaraErrorFmt("failed to read level #{0}", i);
return Nz::Err(ResourceLoadingError::DecodingError);
}
if (width > 1)
width >>= 1;
if (height > 1)
height >>= 1;
if (depth > 1)
depth >>= 1;
}
if (parameters.loadFormat != PixelFormat::Undefined)
image->Convert(parameters.loadFormat);
return image;
}
private:
static bool IdentifyImageType(const DDSHeader& header, const DDSHeaderDX10Ext& headerExt, ImageType* type)
{
if (headerExt.arraySize > 1)
{
if (header.ddsCaps[1] & DDSCAPS2_CUBEMAP)
{
NazaraError("cubemap arrays are not yet supported, sorry");
return false;
}
else if (header.flags & DDSD_HEIGHT)
*type = ImageType::E2D_Array;
else
*type = ImageType::E1D_Array;
}
else
{
if (header.ddsCaps[1] & DDSCAPS2_CUBEMAP)
{
if ((header.ddsCaps[1] & DDSCAPS2_CUBEMAP_ALLFACES) != DDSCAPS2_CUBEMAP_ALLFACES)
{
NazaraError("partial cubemap are not yet supported, sorry");
return false;
}
*type = ImageType::Cubemap;
}
else if (headerExt.resourceDimension == D3D10_RESOURCE_DIMENSION_BUFFER)
{
NazaraError("texture buffers are not yet supported, sorry");
return false;
}
else if (headerExt.resourceDimension == D3D10_RESOURCE_DIMENSION_TEXTURE1D)
*type = ImageType::E1D;
else if (header.ddsCaps[1] & DDSCAPS2_VOLUME || header.flags & DDSD_DEPTH || headerExt.resourceDimension == D3D10_RESOURCE_DIMENSION_TEXTURE3D)
*type = ImageType::E3D;
else
*type = ImageType::E2D;
}
return true;
}
static bool IdentifyPixelFormat(const DDSHeader& header, const DDSHeaderDX10Ext& headerExt, PixelFormat* format)
{
if (header.format.flags & (DDPF_RGB | DDPF_ALPHA | DDPF_ALPHAPIXELS | DDPF_LUMINANCE))
{
PixelFormatDescription info(PixelFormatContent::ColorRGBA, SafeCast<UInt8>(header.format.bpp), PixelFormatSubType::Unsigned);
if (header.format.flags & DDPF_RGB)
{
// Reverse bits for our masks
info.redMask = header.format.redMask;
info.greenMask = header.format.greenMask;
info.blueMask = header.format.blueMask;
}
else if (header.format.flags & DDPF_LUMINANCE)
info.redMask = header.format.redMask;
if (header.format.flags & (DDPF_ALPHA | DDPF_ALPHAPIXELS))
info.alphaMask = header.format.alphaMask;
*format = PixelFormatInfo::IdentifyFormat(info);
if (!PixelFormatInfo::IsValid(*format))
return false;
}
else if (header.format.flags & DDPF_FOURCC)
{
switch (header.format.fourCC)
{
case D3DFMT_DXT1:
*format = PixelFormat::DXT1;
break;
case D3DFMT_DXT3:
*format = PixelFormat::DXT3;
break;
case D3DFMT_DXT5:
*format = PixelFormat::DXT3;
break;
case D3DFMT_DX10:
{
switch (headerExt.dxgiFormat)
{
case DXGI_FORMAT_R32G32B32A32_FLOAT:
*format = PixelFormat::RGBA32F;
break;
case DXGI_FORMAT_R32G32B32A32_UINT:
*format = PixelFormat::RGBA32UI;
break;
case DXGI_FORMAT_R32G32B32A32_SINT:
*format = PixelFormat::RGBA32I;
break;
case DXGI_FORMAT_R32G32B32_FLOAT:
*format = PixelFormat::RGB32F;
break;
case DXGI_FORMAT_R32G32B32_UINT:
//*format = PixelFormat::RGB32U;
return false;
case DXGI_FORMAT_R32G32B32_SINT:
*format = PixelFormat::RGB32I;
break;
case DXGI_FORMAT_R16G16B16A16_SNORM:
case DXGI_FORMAT_R16G16B16A16_SINT:
case DXGI_FORMAT_R16G16B16A16_UINT:
*format = PixelFormat::RGBA16I;
break;
case DXGI_FORMAT_R16G16B16A16_UNORM:
*format = PixelFormat::RGBA16UI;
break;
default:
//TODO
NazaraError("TODO");
break;
}
break;
}
default:
{
char buf[5];
buf[0] = (header.format.fourCC >> 0) & 255;
buf[1] = (header.format.fourCC >> 8) & 255;
buf[2] = (header.format.fourCC >> 16) & 255;
buf[3] = (header.format.fourCC >> 24) & 255;
buf[4] = '\0';
NazaraErrorFmt("unhandled format \"{0}\"", buf);
return false;
}
}
}
else
{
NazaraError("invalid DDS file");
return false;
}
return true;
}
};
namespace Loaders
{
ImageLoader::Entry GetImageLoader_DDS()
{
ImageLoader::Entry loaderEntry;
loaderEntry.extensionSupport = DDSLoader::IsSupported;
loaderEntry.streamLoader = DDSLoader::Load;
loaderEntry.parameterFilter = [](const ImageParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinDDSLoader"); result.GetValueOr(false))
return false;
return true;
};
return loaderEntry;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_DDSLOADER_HPP
#define NAZARA_CORE_FORMATS_DDSLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Image.hpp>
namespace Nz::Loaders
{
ImageLoader::Entry GetImageLoader_DDS();
}
#endif // NAZARA_CORE_FORMATS_DDSLOADER_HPP

View File

@@ -0,0 +1,761 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/GIFLoader.hpp>
#include <Nazara/Core/ByteStream.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Image.hpp>
#include <Nazara/Core/Stream.hpp>
#include <Nazara/Core/Formats/STBLoader.hpp>
#include <NazaraUtils/Bitset.hpp>
#include <NazaraUtils/CallOnExit.hpp>
#include <NazaraUtils/Endianness.hpp>
#include <Nazara/Core/Debug.hpp>
// based on https://www.w3.org/Graphics/GIF/spec-gif89a.txt, with help from the following public domain libraries source code:
// - https://github.com/lecram/gifdec
// - https://github.com/nothings/stb/blob/master/stb_image.h
namespace Nz
{
namespace
{
constexpr UInt8 DisposeToBackground = 2;
constexpr UInt8 DisposeToPrevious = 3;
class GIFImageStream : public ImageStream
{
public:
GIFImageStream()
{
m_byteStream.SetDataEndianness(Endianness::LittleEndian);
}
~GIFImageStream()
{
}
bool Check()
{
std::array<UInt8, 6> header; //< 3 bytes for signature + 3 bytes for version (87a and 89a supported)
if (m_byteStream.Read(header.data(), header.size()) != header.size())
return false;
if (std::memcmp(&header[0], "GIF", 3) != 0)
return false;
if (std::memcmp(&header[3], "87a", 3) != 0 && std::memcmp(&header[3], "89a", 3) != 0)
return false;
return true;
}
bool DecodeNextFrame(void* frameBuffer, Time* frameTime) override
{
if (m_currentFrame >= m_frames.size())
{
if (frameTime)
*frameTime = m_endFrameTime;
return false;
}
UInt8* outputImage = static_cast<UInt8*>(frameBuffer);
auto& frameData = m_frames[m_currentFrame];
if (frameTime)
*frameTime = frameData.time;
UInt16 left;
UInt16 top;
UInt16 width;
UInt16 height;
UInt8 flag;
m_byteStream.GetStream()->SetCursorPos(frameData.streamOffset);
m_byteStream >> left >> top >> width >> height >> flag;
ImageDecodingData decodingData;
decodingData.lineSize = m_header.width * 4;
decodingData.startX = left * 4;
decodingData.startY = top * decodingData.lineSize;
decodingData.maxX = decodingData.startX + width * 4;
decodingData.maxY = decodingData.startY + decodingData.lineSize * height;
decodingData.currentX = decodingData.startX;
decodingData.currentY = decodingData.startY;
// Render to previous frame if frame history is required
if (m_requiresFrameHistory)
decodingData.outputImage = m_previousFrame.get();
else
decodingData.outputImage = outputImage;
std::size_t pixelCount = m_header.width * m_header.height;
if (m_currentFrame == 0)
{
if (m_requiresFrameHistory)
std::memset(m_previousFrame.get(), 0, pixelCount * 4);
else if (outputImage)
std::memset(outputImage, 0, pixelCount * 4);
if (m_disposedRendering)
std::memset(m_disposedRendering.get(), 0, pixelCount * 4);
}
else if (m_requiresFrameHistory)
{
if (m_frames[m_currentFrame - 1].disposalMethod == DisposeToBackground)
{
// FIXME: Is background color something else than transparent?
std::array<UInt8, 4> backgroundColor;
backgroundColor.fill(0);
// restore affected pixels to background
for (std::size_t i = 0; i < pixelCount; ++i)
{
if (m_affectedPixels[i])
std::memcpy(&m_previousFrame[i * 4], &backgroundColor[0], 4);
}
}
else if (m_frames[m_currentFrame - 1].disposalMethod == DisposeToPrevious)
{
// restore affected pixels to frame N - 2
for (std::size_t i = 0; i < pixelCount; ++i)
{
if (m_affectedPixels[i])
std::memcpy(&m_previousFrame[i * 4], &m_disposedRendering[i * 4], 4);
}
}
if (m_disposedRendering)
std::memcpy(&m_disposedRendering[0], &m_previousFrame[0], pixelCount * 4);
}
else if (m_frames[m_currentFrame - 1].disposalMethod == DisposeToBackground)
{
// Special case where each frame dispose to background but does full rendering
// simply clear to transparent
if (outputImage)
std::memset(outputImage, 0, pixelCount * 4);
}
// if the width of the specified rectangle is 0, that means
// we may not see *any* pixels or the image is malformed;
// to make sure this is caught, move the current y down to
// max_y (which is what out_gif_code checks).
if (width == 0)
decodingData.currentY = decodingData.maxY;
bool interlace = (flag & 0b0100'0000);
if (interlace)
{
decodingData.step = 8 * decodingData.lineSize;
decodingData.parseMode = 3;
}
else
{
decodingData.step = decodingData.lineSize;
decodingData.parseMode = 0;
}
bool hasLocalColorTable = (flag & 0b1000'0000);
if (hasLocalColorTable)
{
UInt16 numEntries = 2ULL << (flag & 0b0000'0111);
m_localColorTable.resize(numEntries);
for (std::size_t i = 0; i < numEntries; ++i)
{
m_byteStream >> m_localColorTable[i].r >> m_localColorTable[i].g >> m_localColorTable[i].b;
m_localColorTable[i].a = 0xFF;
}
decodingData.colorTable = &m_localColorTable[0];
decodingData.transparentColorIndex = frameData.transparentIndex;
}
else if (!m_globalColorTable.empty())
{
decodingData.colorTable = &m_globalColorTable[0];
decodingData.transparentColorIndex = frameData.transparentIndex;
}
else
{
// this error should have been caught already when loading
NazaraInternalError("expected color table");
return false;
}
UInt8 minimumCodeSize;
m_byteStream >> minimumCodeSize;
if (minimumCodeSize > 12)
{
NazaraInternalErrorFmt("unexpected LZW Minimum Code Size ({0})", minimumCodeSize);
return false;
}
if (decodingData.outputImage)
{
if (!DecodeImageDescriptor(minimumCodeSize, decodingData))
return false;
}
else
SkipUntilTerminationBlock();
if (m_currentFrame == 0)
{
// if first frame, any pixel not drawn to gets the background color
if (!m_globalColorTable.empty())
{
for (std::size_t i = 0; i < pixelCount; ++i)
{
if (!m_affectedPixels[i])
{
UInt8* outputPixel = &outputImage[i * 4];
outputPixel[0] = m_globalColorTable[m_header.backgroundPaletteIndex].r;
outputPixel[1] = m_globalColorTable[m_header.backgroundPaletteIndex].g;
outputPixel[2] = m_globalColorTable[m_header.backgroundPaletteIndex].b;
outputPixel[3] = m_globalColorTable[m_header.backgroundPaletteIndex].a;
}
}
}
}
if (outputImage && decodingData.outputImage != outputImage)
std::memcpy(outputImage, decodingData.outputImage, pixelCount * 4);
m_currentFrame++;
return true;
}
UInt64 GetFrameCount() const override
{
return m_frames.size();
}
PixelFormat GetPixelFormat() const override
{
return PixelFormat::RGBA8; //< TODO: Set SRGB
}
Vector2ui GetSize() const override
{
return Vector2ui(m_header.width, m_header.height);
}
void Seek(UInt64 frameIndex) override
{
assert(frameIndex <= m_frames.size());
if (m_requiresFrameHistory)
{
if (m_currentFrame > frameIndex)
m_currentFrame = 0;
while (m_currentFrame < frameIndex)
DecodeNextFrame(nullptr, nullptr);
}
else
m_currentFrame = frameIndex;
}
UInt64 Tell() override
{
return m_currentFrame;
}
Result<void, ResourceLoadingError> Open()
{
if (!Check())
return Err(ResourceLoadingError::Unrecognized);
m_byteStream >> m_header.width >> m_header.height;
m_byteStream >> m_header.flags >> m_header.backgroundPaletteIndex >> m_header.ratio;
bool hasGlobalColorTable = (m_header.flags & 0b1000'0000);
if (hasGlobalColorTable)
{
std::size_t numEntries = 2ULL << (m_header.flags & 0b0000'0111);
m_globalColorTable.resize(numEntries);
for (std::size_t i = 0; i < numEntries; ++i)
{
m_byteStream >> m_globalColorTable[i].r >> m_globalColorTable[i].g >> m_globalColorTable[i].b;
m_globalColorTable[i].a = 0xFF;
}
}
m_frames.clear();
m_requiresFrameHistory = false;
bool hasDisposeToPrevious = false;
bool hasPartialRendering = false;
bool terminated = false;
Time frameTime = Time::Zero();
FrameMetadata nextFrame;
while (!terminated)
{
UInt8 tag;
m_byteStream >> tag;
switch (tag)
{
case 0: //< empty block?
break;
case 0x2C: //< image descriptor tag
{
nextFrame.streamOffset = m_byteStream.GetStream()->GetCursorPos();
m_frames.push_back(nextFrame);
nextFrame = {};
UInt16 left;
UInt16 top;
UInt16 width;
UInt16 height;
UInt8 flag;
m_byteStream >> left >> top >> width >> height >> flag;
if (left + width > m_header.width)
{
NazaraError("corrupt gif (out of range)");
return Err(ResourceLoadingError::DecodingError);
}
if (top + height > m_header.height)
{
NazaraError("corrupt gif (out of range)");
return Err(ResourceLoadingError::DecodingError);
}
if (left != 0 || top != 0 || width < m_header.width || height < m_header.height)
hasPartialRendering = true;
if (flag & 0b1000'0000)
{
// has local color table
UInt16 colorTableSize = 2ULL << (flag & 0b0000'0111);
m_byteStream.Read(nullptr, colorTableSize * 3);
}
else if (!hasGlobalColorTable)
{
NazaraErrorFmt("corrupt gif (no color table for image #{0}", m_frames.size() - 1);
return Err(ResourceLoadingError::DecodingError);
}
UInt8 minimumCodeSize;
m_byteStream >> minimumCodeSize;
if (minimumCodeSize > 12)
{
NazaraErrorFmt("unexpected LZW Minimum Code Size ({0})", minimumCodeSize);
return Err(ResourceLoadingError::DecodingError);
}
SkipUntilTerminationBlock();
break;
}
case 0x3B: //< end of file
terminated = true;
break;
case 0x21: //< extension tag
{
UInt8 label;
m_byteStream >> label;
switch (label)
{
case 0xF9: //< graphic control extension
{
UInt8 blockSize;
UInt8 flags;
UInt16 delay;
m_byteStream >> blockSize >> flags >> delay;
if (delay == 0)
delay = 10;
if (blockSize != 4)
{
NazaraError("corrupt gif (invalid block size for graphic control extension)");
return Err(ResourceLoadingError::DecodingError);
}
nextFrame.disposalMethod = (flags & 0b0001'1100) >> 2;
nextFrame.time = frameTime;
frameTime += Time::Milliseconds(delay * 10);
if (flags & 0b0000'0001)
{
UInt8 transparentIndex;
m_byteStream >> transparentIndex;
nextFrame.transparentIndex = transparentIndex;
}
if (nextFrame.disposalMethod == DisposeToPrevious)
hasDisposeToPrevious = true;
break;
}
case 0xFE: //< comment extension
break;
case 0x01: //< plain text extension
break;
case 0xFF: //< application extension
break;
default:
NazaraWarningFmt("unrecognized extension label (unknown tag {0:#x})", label);
break;
}
SkipUntilTerminationBlock();
break;
}
default:
NazaraErrorFmt("corrupt gif (unknown tag {0:#x})", tag);
return Err(ResourceLoadingError::DecodingError);
}
}
if (hasDisposeToPrevious || hasPartialRendering)
m_requiresFrameHistory = true;
m_endFrameTime = frameTime;
m_affectedPixels.Resize(m_header.width * m_header.height);
if (m_requiresFrameHistory)
m_previousFrame = std::make_unique<UInt8[]>(m_header.width * m_header.height * 4);
else
m_previousFrame.reset();
if (hasDisposeToPrevious)
m_disposedRendering = std::make_unique<UInt8[]>(m_header.width * m_header.height * 4);
else
m_disposedRendering.reset();
m_currentFrame = 0;
return Ok();
}
bool SetFile(const std::filesystem::path& filePath)
{
std::unique_ptr<File> file = std::make_unique<File>();
if (!file->Open(filePath, OpenMode::Read))
{
NazaraErrorFmt("failed to open stream from file: {0}", Error::GetLastError());
return false;
}
m_ownedStream = std::move(file);
SetStream(*m_ownedStream);
return true;
}
void SetMemory(const void* data, std::size_t size)
{
m_ownedStream = std::make_unique<MemoryView>(data, size);
SetStream(*m_ownedStream);
}
void SetStream(Stream& stream)
{
m_byteStream.SetStream(&stream);
}
private:
struct ImageDecodingData;
bool DecodeImageDescriptor(UInt8 minimumCodeSize, ImageDecodingData& decodingData)
{
Int32 clear = 1 << minimumCodeSize;
UInt32 first = 1;
Int32 codeSize = minimumCodeSize + 1;
Int32 codeMask = (1 << codeSize) - 1;
Int32 bits = 0;
Int32 validBits = 0;
m_lzwEntries.clear();
m_lzwEntries.resize(8192); //< ??
for (Int32 i = 0; i < clear; ++i)
{
auto& entry = m_lzwEntries[i];
entry.prefix = -1;
entry.first = UInt8(i);
entry.suffix = UInt8(i);
}
// support no starting clear code
Int32 avail = clear + 2;
Int32 oldcode = -1;
UInt8 len = 0;
m_affectedPixels.Reset();
for (;;)
{
if (validBits < codeSize)
{
if (len == 0)
{
m_byteStream >> len; // start new block
if (len == 0)
break;
}
UInt8 data;
m_byteStream >> data;
--len;
bits |= data << validBits;
validBits += 8;
}
else
{
Int32 code = bits & codeMask;
bits >>= codeSize;
validBits -= codeSize;
// @OPTIMIZE: is there some way we can accelerate the non-clear path?
if (code == clear)
{
// clear code
codeSize = minimumCodeSize + 1;
codeMask = (1 << codeSize) - 1;
avail = clear + 2;
oldcode = -1;
first = 0;
}
else if (code == clear + 1)
{
// end of stream code
SkipUntilTerminationBlock();
break;
}
else if (code <= avail)
{
if (first)
{
NazaraError("corrupt gif (no clear code)");
return false;
}
if (oldcode >= 0)
{
auto& p = m_lzwEntries[avail++];
if (avail > 8192)
{
NazaraError("corrupt gif (too many codes)");
return false;
}
p.prefix = SafeCast<Int16>(oldcode);
p.first = m_lzwEntries[oldcode].first;
p.suffix = (code == avail) ? p.first : m_lzwEntries[code].first;
}
else if (code == avail)
{
NazaraError("corrupt gif (illegal code in raster)");
return false;
}
DecodeGIF(SafeCast<UInt16>(code), decodingData);
if ((avail & codeMask) == 0 && avail <= 0x0FFF)
{
codeSize++;
codeMask = (1 << codeSize) - 1;
}
oldcode = code;
}
else
{
NazaraError("corrupt gif (illegal code in raster)");
return false;
}
}
}
return true;
}
void DecodeGIF(UInt16 code, ImageDecodingData& decodingData)
{
// recurse to decode the prefixes, since the linked-list is backwards,
// and working backwards through an interleaved image would be nasty
if (m_lzwEntries[code].prefix >= 0)
DecodeGIF(m_lzwEntries[code].prefix, decodingData);
if (decodingData.currentY >= decodingData.maxY)
return;
std::size_t idx = decodingData.currentX + decodingData.currentY;
UInt8* p = &decodingData.outputImage[idx];
m_affectedPixels[idx / 4] = true;
std::size_t colorIndex = m_lzwEntries[code].suffix;
const Color* c = &decodingData.colorTable[colorIndex];
// don't render transparent pixels
if (colorIndex != decodingData.transparentColorIndex)
{
p[0] = c->r;
p[1] = c->g;
p[2] = c->b;
p[3] = c->a;
}
decodingData.currentX += 4;
if (decodingData.currentX >= decodingData.maxX)
{
decodingData.currentX = decodingData.startX;
decodingData.currentY += decodingData.step;
while (decodingData.currentY >= decodingData.maxY && decodingData.parseMode > 0)
{
decodingData.step = (1ULL << decodingData.parseMode) * decodingData.lineSize;
decodingData.currentY = decodingData.startY + (decodingData.step >> 1);
--decodingData.parseMode;
}
}
}
void SkipUntilTerminationBlock()
{
for (;;)
{
UInt8 blockSize;
m_byteStream >> blockSize;
if (blockSize == 0)
return;
m_byteStream.Read(nullptr, blockSize);
}
}
struct Color
{
UInt8 r, g, b, a;
};
struct FrameMetadata
{
std::size_t transparentIndex = std::numeric_limits<std::size_t>::max();
Time time;
UInt64 streamOffset;
UInt8 disposalMethod = 0;
};
struct ImageDecodingData
{
std::size_t currentX;
std::size_t currentY;
std::size_t lineSize;
std::size_t maxX;
std::size_t maxY;
std::size_t parseMode;
std::size_t startX;
std::size_t startY;
std::size_t step;
std::size_t transparentColorIndex;
Color* colorTable;
UInt8* outputImage;
};
struct LogicalScreenDescriptor
{
UInt16 height;
UInt16 width;
UInt8 backgroundPaletteIndex;
UInt8 flags;
UInt8 packedFields;
UInt8 ratio;
};
struct LZWEntry
{
Int16 prefix = 0;
UInt8 first = 0;
UInt8 suffix = 0;
};
std::size_t m_currentFrame;
std::vector<Color> m_globalColorTable;
std::vector<Color> m_localColorTable;
std::vector<FrameMetadata> m_frames;
std::vector<LZWEntry> m_lzwEntries;
std::unique_ptr<Stream> m_ownedStream;
std::unique_ptr<UInt8[]> m_disposedRendering;
std::unique_ptr<UInt8[]> m_previousFrame;
Bitset<UInt64> m_affectedPixels;
ByteStream m_byteStream;
LogicalScreenDescriptor m_header;
Time m_endFrameTime;
bool m_requiresFrameHistory;
};
bool CheckGIFExtension(std::string_view extension)
{
return extension == ".gif";
}
Result<std::shared_ptr<ImageStream>, ResourceLoadingError> LoadGIFFile(const std::filesystem::path& filePath, const ImageStreamParams& /*parameters*/)
{
std::shared_ptr<GIFImageStream> gifStream = std::make_shared<GIFImageStream>();
if (!gifStream->SetFile(filePath))
return Err(ResourceLoadingError::FailedToOpenFile);
Result status = gifStream->Open();
return status.Map([&] { return std::move(gifStream); });
}
Result<std::shared_ptr<ImageStream>, ResourceLoadingError> LoadGIFMemory(const void* ptr, std::size_t size, const ImageStreamParams& /*parameters*/)
{
std::shared_ptr<GIFImageStream> gifStream = std::make_shared<GIFImageStream>();
gifStream->SetMemory(ptr, size);
Result status = gifStream->Open();
return status.Map([&] { return std::move(gifStream); });
}
Result<std::shared_ptr<ImageStream>, ResourceLoadingError> LoadGIFStream(Stream& stream, const ImageStreamParams& /*parameters*/)
{
std::shared_ptr<GIFImageStream> gifStream = std::make_shared<GIFImageStream>();
gifStream->SetStream(stream);
Result status = gifStream->Open();
return status.Map([&] { return std::move(gifStream); });
}
}
namespace Loaders
{
ImageStreamLoader::Entry GetImageStreamLoader_GIF()
{
ImageStreamLoader::Entry loaderEntry;
loaderEntry.extensionSupport = CheckGIFExtension;
loaderEntry.fileLoader = LoadGIFFile;
loaderEntry.memoryLoader = LoadGIFMemory;
loaderEntry.streamLoader = LoadGIFStream;
loaderEntry.parameterFilter = [](const ImageStreamParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinGIFLoader"); result.GetValueOr(false))
return false;
return true;
};
return loaderEntry;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_GIFLOADER_HPP
#define NAZARA_CORE_FORMATS_GIFLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Image.hpp>
#include <Nazara/Core/ImageStream.hpp>
namespace Nz::Loaders
{
ImageStreamLoader::Entry GetImageStreamLoader_GIF();
}
#endif // NAZARA_CORE_FORMATS_GIFLOADER_HPP

View File

@@ -0,0 +1,177 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MD2Constants.hpp>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
const UInt32 md2Ident = 'I' + ('D'<<8) + ('P'<<16) + ('2'<<24);
const Vector3f md2Normals[162] =
{
Vector3f(-0.525731f, 0.000000f, 0.850651f),
Vector3f(-0.442863f, 0.238856f, 0.864188f),
Vector3f(-0.295242f, 0.000000f, 0.955423f),
Vector3f(-0.309017f, 0.500000f, 0.809017f),
Vector3f(-0.162460f, 0.262866f, 0.951056f),
Vector3f(0.000000f, 0.000000f, 1.000000f),
Vector3f(0.000000f, 0.850651f, 0.525731f),
Vector3f(-0.147621f, 0.716567f, 0.681718f),
Vector3f(0.147621f, 0.716567f, 0.681718f),
Vector3f(0.000000f, 0.525731f, 0.850651f),
Vector3f(0.309017f, 0.500000f, 0.809017f),
Vector3f(0.525731f, 0.000000f, 0.850651f),
Vector3f(0.295242f, 0.000000f, 0.955423f),
Vector3f(0.442863f, 0.238856f, 0.864188f),
Vector3f(0.162460f, 0.262866f, 0.951056f),
Vector3f(-0.681718f, 0.147621f, 0.716567f),
Vector3f(-0.809017f, 0.309017f, 0.500000f),
Vector3f(-0.587785f, 0.425325f, 0.688191f),
Vector3f(-0.850651f, 0.525731f, 0.000000f),
Vector3f(-0.864188f, 0.442863f, 0.238856f),
Vector3f(-0.716567f, 0.681718f, 0.147621f),
Vector3f(-0.688191f, 0.587785f, 0.425325f),
Vector3f(-0.500000f, 0.809017f, 0.309017f),
Vector3f(-0.238856f, 0.864188f, 0.442863f),
Vector3f(-0.425325f, 0.688191f, 0.587785f),
Vector3f(-0.716567f, 0.681718f, -0.147621f),
Vector3f(-0.500000f, 0.809017f, -0.309017f),
Vector3f(-0.525731f, 0.850651f, 0.000000f),
Vector3f(0.000000f, 0.850651f, -0.525731f),
Vector3f(-0.238856f, 0.864188f, -0.442863f),
Vector3f(0.000000f, 0.955423f, -0.295242f),
Vector3f(-0.262866f, 0.951056f, -0.162460f),
Vector3f(0.000000f, 1.000000f, 0.000000f),
Vector3f(0.000000f, 0.955423f, 0.295242f),
Vector3f(-0.262866f, 0.951056f, 0.162460f),
Vector3f(0.238856f, 0.864188f, 0.442863f),
Vector3f(0.262866f, 0.951056f, 0.162460f),
Vector3f(0.500000f, 0.809017f, 0.309017f),
Vector3f(0.238856f, 0.864188f, -0.442863f),
Vector3f(0.262866f, 0.951056f, -0.162460f),
Vector3f(0.500000f, 0.809017f, -0.309017f),
Vector3f(0.850651f, 0.525731f, 0.000000f),
Vector3f(0.716567f, 0.681718f, 0.147621f),
Vector3f(0.716567f, 0.681718f, -0.147621f),
Vector3f(0.525731f, 0.850651f, 0.000000f),
Vector3f(0.425325f, 0.688191f, 0.587785f),
Vector3f(0.864188f, 0.442863f, 0.238856f),
Vector3f(0.688191f, 0.587785f, 0.425325f),
Vector3f(0.809017f, 0.309017f, 0.500000f),
Vector3f(0.681718f, 0.147621f, 0.716567f),
Vector3f(0.587785f, 0.425325f, 0.688191f),
Vector3f(0.955423f, 0.295242f, 0.000000f),
Vector3f(1.000000f, 0.000000f, 0.000000f),
Vector3f(0.951056f, 0.162460f, 0.262866f),
Vector3f(0.850651f, -0.525731f, 0.000000f),
Vector3f(0.955423f, -0.295242f, 0.000000f),
Vector3f(0.864188f, -0.442863f, 0.238856f),
Vector3f(0.951056f, -0.162460f, 0.262866f),
Vector3f(0.809017f, -0.309017f, 0.500000f),
Vector3f(0.681718f, -0.147621f, 0.716567f),
Vector3f(0.850651f, 0.000000f, 0.525731f),
Vector3f(0.864188f, 0.442863f, -0.238856f),
Vector3f(0.809017f, 0.309017f, -0.500000f),
Vector3f(0.951056f, 0.162460f, -0.262866f),
Vector3f(0.525731f, 0.000000f, -0.850651f),
Vector3f(0.681718f, 0.147621f, -0.716567f),
Vector3f(0.681718f, -0.147621f, -0.716567f),
Vector3f(0.850651f, 0.000000f, -0.525731f),
Vector3f(0.809017f, -0.309017f, -0.500000f),
Vector3f(0.864188f, -0.442863f, -0.238856f),
Vector3f(0.951056f, -0.162460f, -0.262866f),
Vector3f(0.147621f, 0.716567f, -0.681718f),
Vector3f(0.309017f, 0.500000f, -0.809017f),
Vector3f(0.425325f, 0.688191f, -0.587785f),
Vector3f(0.442863f, 0.238856f, -0.864188f),
Vector3f(0.587785f, 0.425325f, -0.688191f),
Vector3f(0.688191f, 0.587785f, -0.425325f),
Vector3f(-0.147621f, 0.716567f, -0.681718f),
Vector3f(-0.309017f, 0.500000f, -0.809017f),
Vector3f(0.000000f, 0.525731f, -0.850651f),
Vector3f(-0.525731f, 0.000000f, -0.850651f),
Vector3f(-0.442863f, 0.238856f, -0.864188f),
Vector3f(-0.295242f, 0.000000f, -0.955423f),
Vector3f(-0.162460f, 0.262866f, -0.951056f),
Vector3f(0.000000f, 0.000000f, -1.000000f),
Vector3f(0.295242f, 0.000000f, -0.955423f),
Vector3f(0.162460f, 0.262866f, -0.951056f),
Vector3f(-0.442863f, -0.238856f, -0.864188f),
Vector3f(-0.309017f, -0.500000f, -0.809017f),
Vector3f(-0.162460f, -0.262866f, -0.951056f),
Vector3f(0.000000f, -0.850651f, -0.525731f),
Vector3f(-0.147621f, -0.716567f, -0.681718f),
Vector3f(0.147621f, -0.716567f, -0.681718f),
Vector3f(0.000000f, -0.525731f, -0.850651f),
Vector3f(0.309017f, -0.500000f, -0.809017f),
Vector3f(0.442863f, -0.238856f, -0.864188f),
Vector3f(0.162460f, -0.262866f, -0.951056f),
Vector3f(0.238856f, -0.864188f, -0.442863f),
Vector3f(0.500000f, -0.809017f, -0.309017f),
Vector3f(0.425325f, -0.688191f, -0.587785f),
Vector3f(0.716567f, -0.681718f, -0.147621f),
Vector3f(0.688191f, -0.587785f, -0.425325f),
Vector3f(0.587785f, -0.425325f, -0.688191f),
Vector3f(0.000000f, -0.955423f, -0.295242f),
Vector3f(0.000000f, -1.000000f, 0.000000f),
Vector3f(0.262866f, -0.951056f, -0.162460f),
Vector3f(0.000000f, -0.850651f, 0.525731f),
Vector3f(0.000000f, -0.955423f, 0.295242f),
Vector3f(0.238856f, -0.864188f, 0.442863f),
Vector3f(0.262866f, -0.951056f, 0.162460f),
Vector3f(0.500000f, -0.809017f, 0.309017f),
Vector3f(0.716567f, -0.681718f, 0.147621f),
Vector3f(0.525731f, -0.850651f, 0.000000f),
Vector3f(-0.238856f, -0.864188f, -0.442863f),
Vector3f(-0.500000f, -0.809017f, -0.309017f),
Vector3f(-0.262866f, -0.951056f, -0.162460f),
Vector3f(-0.850651f, -0.525731f, 0.000000f),
Vector3f(-0.716567f, -0.681718f, -0.147621f),
Vector3f(-0.716567f, -0.681718f, 0.147621f),
Vector3f(-0.525731f, -0.850651f, 0.000000f),
Vector3f(-0.500000f, -0.809017f, 0.309017f),
Vector3f(-0.238856f, -0.864188f, 0.442863f),
Vector3f(-0.262866f, -0.951056f, 0.162460f),
Vector3f(-0.864188f, -0.442863f, 0.238856f),
Vector3f(-0.809017f, -0.309017f, 0.500000f),
Vector3f(-0.688191f, -0.587785f, 0.425325f),
Vector3f(-0.681718f, -0.147621f, 0.716567f),
Vector3f(-0.442863f, -0.238856f, 0.864188f),
Vector3f(-0.587785f, -0.425325f, 0.688191f),
Vector3f(-0.309017f, -0.500000f, 0.809017f),
Vector3f(-0.147621f, -0.716567f, 0.681718f),
Vector3f(-0.425325f, -0.688191f, 0.587785f),
Vector3f(-0.162460f, -0.262866f, 0.951056f),
Vector3f(0.442863f, -0.238856f, 0.864188f),
Vector3f(0.162460f, -0.262866f, 0.951056f),
Vector3f(0.309017f, -0.500000f, 0.809017f),
Vector3f(0.147621f, -0.716567f, 0.681718f),
Vector3f(0.000000f, -0.525731f, 0.850651f),
Vector3f(0.425325f, -0.688191f, 0.587785f),
Vector3f(0.587785f, -0.425325f, 0.688191f),
Vector3f(0.688191f, -0.587785f, 0.425325f),
Vector3f(-0.955423f, 0.295242f, 0.000000f),
Vector3f(-0.951056f, 0.162460f, 0.262866f),
Vector3f(-1.000000f, 0.000000f, 0.000000f),
Vector3f(-0.850651f, 0.000000f, 0.525731f),
Vector3f(-0.955423f, -0.295242f, 0.000000f),
Vector3f(-0.951056f, -0.162460f, 0.262866f),
Vector3f(-0.864188f, 0.442863f, -0.238856f),
Vector3f(-0.951056f, 0.162460f, -0.262866f),
Vector3f(-0.809017f, 0.309017f, -0.500000f),
Vector3f(-0.864188f, -0.442863f, -0.238856f),
Vector3f(-0.951056f, -0.162460f, -0.262866f),
Vector3f(-0.809017f, -0.309017f, -0.500000f),
Vector3f(-0.681718f, 0.147621f, -0.716567f),
Vector3f(-0.681718f, -0.147621f, -0.716567f),
Vector3f(-0.850651f, 0.000000f, -0.525731f),
Vector3f(-0.688191f, 0.587785f, -0.425325f),
Vector3f(-0.587785f, 0.425325f, -0.688191f),
Vector3f(-0.425325f, 0.688191f, -0.587785f),
Vector3f(-0.425325f, -0.688191f, -0.587785f),
Vector3f(-0.587785f, -0.425325f, -0.688191f),
Vector3f(-0.688191f, -0.587785f, -0.425325f)
};
}

View File

@@ -0,0 +1,69 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_MD2CONSTANTS_HPP
#define NAZARA_CORE_FORMATS_MD2CONSTANTS_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Math/Vector3.hpp>
namespace Nz
{
struct MD2_Header
{
UInt32 ident; // nombre magique : "IDP2"
UInt32 version; // version du format : 8
UInt32 skinwidth; // largeur texture
UInt32 skinheight; // hauteur texture
UInt32 framesize; // taille d'une frame en octets
UInt32 num_skins; // nombre de skins
UInt32 num_vertices; // nombre de vertices par frame
UInt32 num_st; // nombre de coordonnées de texture
UInt32 num_tris; // nombre de triangles
UInt32 num_glcmds; // nombre de commandes opengl
UInt32 num_frames; // nombre de frames
UInt32 offset_skins; // offset données skins
UInt32 offset_st; // offset données coordonnées de texture
UInt32 offset_tris; // offset données triangles
UInt32 offset_frames; // offset données frames
UInt32 offset_glcmds; // offset données commandes OpenGL
UInt32 offset_end; // offset fin de fichier
};
static_assert(sizeof(MD2_Header) == 17*sizeof(UInt32), "MD2_Header must be packed");
struct MD2_Vertex
{
UInt8 x, y, z;
UInt8 n;
};
static_assert(sizeof(MD2_Vertex) == 4*sizeof(UInt8), "MD2_Vertex must be packed");
struct MD2_TexCoord
{
Int16 u, v;
};
static_assert(sizeof(MD2_TexCoord) == 2*sizeof(Int16), "MD2_TexCoord must be packed");
struct MD2_Triangle
{
UInt16 vertices[3];
UInt16 texCoords[3];
};
static_assert(sizeof(MD2_Triangle) == 2*3*sizeof(UInt16), "MD2_Triangle must be packed");
extern const UInt32 md2Ident;
extern const Vector3f md2Normals[162];
}
#endif // NAZARA_CORE_FORMATS_MD2CONSTANTS_HPP

View File

@@ -0,0 +1,271 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MD2Loader.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/MaterialData.hpp>
#include <Nazara/Core/Mesh.hpp>
#include <Nazara/Core/StaticMesh.hpp>
#include <Nazara/Core/Stream.hpp>
#include <Nazara/Core/VertexMapper.hpp>
#include <Nazara/Core/Formats/MD2Constants.hpp>
#include <Nazara/Math/Quaternion.hpp>
#include <NazaraUtils/Endianness.hpp>
#include <array>
#include <cassert>
#include <memory>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace
{
bool IsMD2Supported(std::string_view extension)
{
return (extension == ".md2");
}
Result<std::shared_ptr<Mesh>, ResourceLoadingError> LoadMD2(Stream& stream, const MeshParams& parameters)
{
MD2_Header header;
if (stream.Read(&header, sizeof(MD2_Header)) != sizeof(MD2_Header))
return Err(ResourceLoadingError::Unrecognized);
#ifdef NAZARA_BIG_ENDIAN
header.ident = ByteSwap(header.ident);
header.version = ByteSwap(header.version);
#endif
if (header.ident != md2Ident)
return Err(ResourceLoadingError::Unrecognized);
if (header.version != 8)
return Err(ResourceLoadingError::Unsupported);
#ifdef NAZARA_BIG_ENDIAN
header.skinwidth = ByteSwap(header.skinwidth);
header.skinheight = ByteSwap(header.skinheight);
header.framesize = ByteSwap(header.framesize);
header.num_skins = ByteSwap(header.num_skins);
header.num_vertices = ByteSwap(header.num_vertices);
header.num_st = ByteSwap(header.num_st);
header.num_tris = ByteSwap(header.num_tris);
header.num_glcmds = ByteSwap(header.num_glcmds);
header.num_frames = ByteSwap(header.num_frames);
header.offset_skins = ByteSwap(header.offset_skins);
header.offset_st = ByteSwap(header.offset_st);
header.offset_tris = ByteSwap(header.offset_tris);
header.offset_frames = ByteSwap(header.offset_frames);
header.offset_glcmds = ByteSwap(header.offset_glcmds);
header.offset_end = ByteSwap(header.offset_end);
#endif
if (stream.GetSize() < header.offset_end)
{
NazaraError("incomplete MD2 file");
return Err(ResourceLoadingError::DecodingError);
}
// Since the engine no longer supports keyframe animations, let's make a static mesh
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
if (!mesh->CreateStatic())
{
NazaraInternalError("Failed to create mesh");
return Err(ResourceLoadingError::Internal);
}
// Extract skins (texture name)
if (header.num_skins > 0)
{
mesh->SetMaterialCount(header.num_skins);
stream.SetCursorPos(header.offset_skins);
{
std::filesystem::path baseDir = stream.GetDirectory();
char skin[68];
for (unsigned int i = 0; i < header.num_skins; ++i)
{
stream.Read(skin, 68*sizeof(char));
ParameterList matData;
matData.SetParameter(MaterialData::BaseColorTexturePath, PathToString(baseDir / skin));
matData.SetParameter(MaterialData::Type, "Phong");
mesh->SetMaterialData(i, std::move(matData));
}
}
}
std::shared_ptr<IndexBuffer> indexBuffer = std::make_shared<IndexBuffer>(IndexType::U16, 3 * header.num_tris, parameters.indexBufferFlags, parameters.bufferFactory);
// Extract triangles data
std::vector<MD2_Triangle> triangles(header.num_tris);
stream.SetCursorPos(header.offset_tris);
stream.Read(&triangles[0], header.num_tris*sizeof(MD2_Triangle));
// And convert them into an index buffer
BufferMapper<IndexBuffer> indexMapper(*indexBuffer, 0, indexBuffer->GetIndexCount());
UInt16* index = static_cast<UInt16*>(indexMapper.GetPointer());
for (unsigned int i = 0; i < header.num_tris; ++i)
{
#ifdef NAZARA_BIG_ENDIAN
triangles[i].vertices[0] = ByteSwap(triangles[i].vertices[0]);
triangles[i].texCoords[0] = ByteSwap(triangles[i].texCoords[0]);
triangles[i].vertices[1] = ByteSwap(triangles[i].vertices[1]);
triangles[i].texCoords[1] = ByteSwap(triangles[i].texCoords[1]);
triangles[i].vertices[2] = ByteSwap(triangles[i].vertices[2]);
triangles[i].texCoords[2] = ByteSwap(triangles[i].texCoords[2]);
#endif
// Reverse winding order
*index++ = triangles[i].vertices[0];
*index++ = triangles[i].vertices[2];
*index++ = triangles[i].vertices[1];
}
indexMapper.Unmap();
// Optimize if requested (improves cache locality)
if (parameters.optimizeIndexBuffers)
indexBuffer->Optimize();
// Extracting texture coordinates
std::vector<MD2_TexCoord> texCoords(header.num_st);
stream.SetCursorPos(header.offset_st);
stream.Read(&texCoords[0], header.num_st*sizeof(MD2_TexCoord));
#ifdef NAZARA_BIG_ENDIAN
for (unsigned int i = 0; i < header.num_st; ++i)
{
texCoords[i].u = ByteSwap(texCoords[i].u);
texCoords[i].v = ByteSwap(texCoords[i].v);
}
#endif
std::shared_ptr<VertexBuffer> vertexBuffer = std::make_shared<VertexBuffer>(parameters.vertexDeclaration, header.num_vertices, parameters.vertexBufferFlags, parameters.bufferFactory);
std::shared_ptr<StaticMesh> subMesh = std::make_shared<StaticMesh>(vertexBuffer, indexBuffer);
// Extracting vertices
stream.SetCursorPos(header.offset_frames);
std::vector<MD2_Vertex> vertices(header.num_vertices);
Vector3f scale, translate;
stream.Read(&scale, sizeof(Vector3f));
stream.Read(&translate, sizeof(Vector3f));
stream.Read(nullptr, 16*sizeof(char)); //< Frame name, unused
stream.Read(vertices.data(), header.num_vertices*sizeof(MD2_Vertex));
#ifdef NAZARA_BIG_ENDIAN
scale.x = ByteSwap(scale.x);
scale.y = ByteSwap(scale.y);
scale.z = ByteSwap(scale.z);
translate.x = ByteSwap(translate.x);
translate.y = ByteSwap(translate.y);
translate.z = ByteSwap(translate.z);
#endif
constexpr float ScaleAdjust = 1.f / 27.8f; // Make a 50 Quake 2 units character a 1.8 unit long
scale *= ScaleAdjust;
scale *= parameters.vertexScale;
translate *= ScaleAdjust;
translate += parameters.vertexOffset;
// Align the model to our coordinates system
Quaternionf rotation = EulerAnglesf(-90.f, 90.f, 0.f);
rotation *= parameters.vertexRotation;
VertexMapper vertexMapper(*vertexBuffer);
// Loading texture coordinates
if (auto uvPtr = vertexMapper.GetComponentPtr<Vector2f>(VertexComponent::TexCoord))
{
constexpr std::array<UInt32, 3> indexFix = {0, 2, 1};
Vector2f invSkinSize(1.f / header.skinwidth, 1.f / header.skinheight);
for (UInt32 i = 0; i < header.num_tris; ++i)
{
for (UInt32 fixedIndex : indexFix) //< Reverse winding order
{
const MD2_TexCoord& texC = texCoords[triangles[i].texCoords[fixedIndex]];
Vector2f uv(texC.u, texC.v);
uv *= invSkinSize;
uvPtr[triangles[i].vertices[fixedIndex]] = parameters.texCoordOffset + uv * parameters.texCoordScale;
}
}
}
// Vertex normals
if (auto normalPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Normal))
{
for (UInt32 v = 0; v < header.num_vertices; ++v)
{
const MD2_Vertex& vert = vertices[v];
*normalPtr++ = TransformNormalTRS(rotation, scale, md2Normals[vert.n]);
}
}
// Vertex positions
if (auto posPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Position))
{
for (UInt32 v = 0; v < header.num_vertices; ++v)
{
const MD2_Vertex& vert = vertices[v];
*posPtr++ = TransformPositionTRS(translate, rotation, scale, Vector3f(vert.x, vert.y, vert.z));
}
}
// Vertex colors (.md2 files have no vertex color)
if (auto colorPtr = vertexMapper.GetComponentPtr<Color>(VertexComponent::Color))
{
for (UInt32 v = 0; v < header.num_vertices; ++v)
*colorPtr++ = Color::White();
}
vertexMapper.Unmap();
subMesh->SetIndexBuffer(std::move(indexBuffer));
subMesh->SetMaterialIndex(0);
subMesh->GenerateAABB();
if (parameters.vertexDeclaration->HasComponentOfType<Vector3f>(VertexComponent::Tangent))
subMesh->GenerateTangents();
mesh->AddSubMesh(subMesh);
if (parameters.center)
mesh->Recenter();
return mesh;
}
}
namespace Loaders
{
MeshLoader::Entry GetMeshLoader_MD2()
{
MeshLoader::Entry loader;
loader.extensionSupport = IsMD2Supported;
loader.streamLoader = LoadMD2;
loader.parameterFilter = [](const MeshParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinMD2Loader"); result.GetValueOr(false))
return false;
return true;
};
return loader;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_MD2LOADER_HPP
#define NAZARA_CORE_FORMATS_MD2LOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Mesh.hpp>
namespace Nz::Loaders
{
MeshLoader::Entry GetMeshLoader_MD2();
}
#endif // NAZARA_CORE_FORMATS_MD2LOADER_HPP

View File

@@ -0,0 +1,112 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MD5AnimLoader.hpp>
#include <Nazara/Core/Animation.hpp>
#include <Nazara/Core/Sequence.hpp>
#include <Nazara/Core/Formats/MD5AnimParser.hpp>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace
{
bool IsMD5AnimSupported(std::string_view extension)
{
return extension == ".md5anim";
}
Result<std::shared_ptr<Animation>, ResourceLoadingError> LoadMD5Anim(Stream& stream, const AnimationParams& /*parameters*/)
{
// TODO: Use parameters
MD5AnimParser parser(stream);
UInt64 streamPos = stream.GetCursorPos();
if (!parser.Check())
return Err(ResourceLoadingError::Unrecognized);
stream.SetCursorPos(streamPos);
if (!parser.Parse())
{
NazaraError("MD5Anim parser failed");
return Err(ResourceLoadingError::DecodingError);
}
const MD5AnimParser::Frame* frames = parser.GetFrames();
UInt32 frameCount = parser.GetFrameCount();
UInt32 frameRate = parser.GetFrameRate();
const MD5AnimParser::Joint* joints = parser.GetJoints();
UInt32 jointCount = parser.GetJointCount();
// À ce stade, nous sommes censés avoir assez d'informations pour créer l'animation
std::shared_ptr<Animation> animation = std::make_shared<Animation>();
animation->CreateSkeletal(frameCount, jointCount);
Sequence sequence;
sequence.firstFrame = 0;
sequence.frameCount = frameCount;
sequence.frameRate = frameRate;
sequence.name = PathToString(stream.GetPath().filename());
animation->AddSequence(sequence);
SequenceJoint* sequenceJoints = animation->GetSequenceJoints();
// Pour que le squelette soit correctement aligné, il faut appliquer un quaternion "de correction" aux joints à la base du squelette
Quaternionf rotationQuat = Quaternionf::RotationBetween(Vector3f::UnitX(), Vector3f::Forward()) *
Quaternionf::RotationBetween(Vector3f::UnitZ(), Vector3f::Up());
//Matrix4f matrix = Matrix4f::Transform(Nz::Vector3f::Zero(), rotationQuat, Vector3f(1.f / 40.f));
//matrix *= parameters.matrix;
rotationQuat = Quaternionf::Identity();
for (UInt32 frameIndex = 0; frameIndex < frameCount; ++frameIndex)
{
for (UInt32 jointIndex = 0; jointIndex < jointCount; ++jointIndex)
{
SequenceJoint& sequenceJoint = sequenceJoints[frameIndex * jointCount + jointIndex];
Int32 parentId = joints[jointIndex].parent;
if (parentId >= 0)
{
sequenceJoint.position = frames[frameIndex].joints[jointIndex].pos;
sequenceJoint.rotation = frames[frameIndex].joints[jointIndex].orient;
}
else
{
sequenceJoint.position = rotationQuat * frames[frameIndex].joints[jointIndex].pos;
sequenceJoint.rotation = rotationQuat * frames[frameIndex].joints[jointIndex].orient;
}
sequenceJoint.scale = Vector3f::Unit();
}
}
return animation;
}
}
namespace Loaders
{
AnimationLoader::Entry GetAnimationLoader_MD5Anim()
{
AnimationLoader::Entry loader;
loader.extensionSupport = IsMD5AnimSupported;
loader.streamLoader = LoadMD5Anim;
loader.parameterFilter = [](const AnimationParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinMD5AnimLoader"); result.GetValueOr(false))
return false;
return true;
};
return loader;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_MD5ANIMLOADER_HPP
#define NAZARA_CORE_FORMATS_MD5ANIMLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Animation.hpp>
namespace Nz::Loaders
{
AnimationLoader::Entry GetAnimationLoader_MD5Anim();
}
#endif // NAZARA_CORE_FORMATS_MD5ANIMLOADER_HPP

View File

@@ -0,0 +1,522 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MD5AnimParser.hpp>
#include <Nazara/Core/Config.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Stream.hpp>
#include <Nazara/Core/StringExt.hpp>
#include <cstdio>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
MD5AnimParser::MD5AnimParser(Stream& stream) :
m_stream(stream),
m_streamFlags(stream.GetStreamOptions()), //< Saves stream flags
m_keepLastLine(false),
m_frameIndex(0),
m_frameRate(0),
m_lineCount(0)
{
m_stream.EnableTextMode(true);
}
MD5AnimParser::~MD5AnimParser()
{
// Reset stream flags
if ((m_streamFlags & StreamOption::Text) == 0)
m_stream.EnableTextMode(false);
}
bool MD5AnimParser::Check()
{
if (Advance(false))
{
unsigned int version;
if (std::sscanf(&m_currentLine[0], " MD5Version %u", &version) == 1)
{
if (version == 10)
return true;
}
}
return false;
}
UInt32 MD5AnimParser::GetAnimatedComponentCount() const
{
return static_cast<UInt32>(m_animatedComponents.size());
}
const MD5AnimParser::Frame* MD5AnimParser::GetFrames() const
{
return m_frames.data();
}
UInt32 MD5AnimParser::GetFrameCount() const
{
return static_cast<UInt32>(m_frames.size());
}
UInt32 MD5AnimParser::GetFrameRate() const
{
return m_frameRate;
}
const MD5AnimParser::Joint* MD5AnimParser::GetJoints() const
{
return m_joints.data();
}
UInt32 MD5AnimParser::GetJointCount() const
{
return static_cast<UInt32>(m_joints.size());
}
bool MD5AnimParser::Parse()
{
while (Advance(false))
{
switch (m_currentLine[0])
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
case 'M': // MD5Version
if (GetWord(m_currentLine, 0) != "MD5Version")
UnrecognizedLine();
break;
#endif
case 'b': // baseframe/bounds
if (StartsWith(m_currentLine, "baseframe {"))
{
if (!ParseBaseframe())
{
Error("Failed to parse baseframe");
return false;
}
}
else if (StartsWith(m_currentLine, "bounds {"))
{
if (!ParseBounds())
{
Error("Failed to parse bounds");
return false;
}
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
break;
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
case 'c': // commandline
if (GetWord(m_currentLine, 0) != "commandline")
UnrecognizedLine();
break;
#endif
case 'f':
{
unsigned int index;
if (std::sscanf(&m_currentLine[0], "frame %u {", &index) == 1)
{
if (m_frameIndex != index)
{
Error("Unexpected frame index (expected " + NumberToString(m_frameIndex) + ", got " + NumberToString(index) + ')');
return false;
}
if (!ParseFrame())
{
Error("Failed to parse frame");
return false;
}
m_frameIndex++;
}
else if (std::sscanf(&m_currentLine[0], "frameRate %u", &m_frameRate) != 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
}
break;
}
case 'h': // hierarchy
if (StartsWith(m_currentLine, "hierarchy {"))
{
if (!ParseHierarchy())
{
Error("Failed to parse hierarchy");
return false;
}
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
break;
case 'n': // num[Frames/Joints]
{
unsigned int count;
if (std::sscanf(&m_currentLine[0], "numAnimatedComponents %u", &count) == 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!m_animatedComponents.empty())
Warning("Animated components count is already defined");
#endif
m_animatedComponents.resize(count);
}
else if (std::sscanf(&m_currentLine[0], "numFrames %u", &count) == 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!m_frames.empty())
Warning("Frame count is already defined");
#endif
m_frames.resize(count);
}
else if (std::sscanf(&m_currentLine[0], "numJoints %u", &count) == 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!m_joints.empty())
Warning("Joint count is already defined");
#endif
m_joints.resize(count);
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
break;
}
default:
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
break;
}
}
std::size_t frameCount = m_frames.size();
if (frameCount == 0)
{
NazaraError("frame count is invalid or missing");
return false;
}
std::size_t jointCount = m_joints.size();
if (jointCount == 0)
{
NazaraError("joint count is invalid or missing");
return false;
}
if (m_frameIndex != frameCount)
{
NazaraErrorFmt("missing frame infos: [{0},{1}]", m_frameIndex, frameCount);
return false;
}
if (m_frameRate == 0)
{
NazaraWarning("framerate is either invalid or missing, assuming a default value of 24");
m_frameRate = 24;
}
return true;
}
bool MD5AnimParser::Advance(bool required)
{
if (!m_keepLastLine)
{
do
{
if (m_stream.EndOfStream())
{
if (required)
Error("Incomplete MD5 file");
return false;
}
m_lineCount++;
m_currentLine = m_stream.ReadLine();
if (std::size_t pos = m_currentLine.find("//"); pos != std::string::npos)
m_currentLine.resize(pos);
}
while (m_currentLine.empty());
}
else
m_keepLastLine = false;
return true;
}
void MD5AnimParser::Error(std::string_view message)
{
NazaraErrorFmt("{0} at line #{1}", message, m_lineCount);
}
bool MD5AnimParser::ParseBaseframe()
{
std::size_t jointCount = m_joints.size();
if (jointCount == 0)
{
Error("Joint count is invalid or missing");
return false;
}
for (std::size_t i = 0; i < jointCount; ++i)
{
if (!Advance())
return false;
// Space is important for the buffer of \n
if (std::sscanf(&m_currentLine[0], " ( %f %f %f ) ( %f %f %f )", &m_joints[i].bindPos.x, &m_joints[i].bindPos.y, &m_joints[i].bindPos.z,
&m_joints[i].bindOrient.x, &m_joints[i].bindOrient.y, &m_joints[i].bindOrient.z) != 6)
{
UnrecognizedLine(true);
return false;
}
}
if (!Advance())
return false;
if (m_currentLine != "}")
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
Warning("Bounds braces closing not found");
#endif
// On tente de survivre à l'erreur
m_keepLastLine = true;
}
return true;
}
bool MD5AnimParser::ParseBounds()
{
std::size_t frameCount = m_frames.size();
if (frameCount == 0)
{
Error("Frame count is invalid or missing");
return false;
}
for (std::size_t i = 0; i < frameCount; ++i)
{
if (!Advance())
return false;
Vector3f min, max;
// Space is important for the buffer of \n
if (std::sscanf(&m_currentLine[0], " ( %f %f %f ) ( %f %f %f )", &min.x, &min.y, &min.z, &max.x, &max.y, &max.z) != 6)
{
UnrecognizedLine(true);
return false;
}
m_frames[i].bounds = Boxf::FromExtents(min, max);
}
if (!Advance())
return false;
if (m_currentLine != "}")
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
Warning("Bounds braces closing not found");
#endif
// On tente de survivre à l'erreur
m_keepLastLine = true;
}
return true;
}
bool MD5AnimParser::ParseFrame()
{
std::size_t animatedComponentsCount = m_animatedComponents.size();
if (animatedComponentsCount == 0)
{
Error("Animated components count is missing or invalid");
return false;
}
std::size_t jointCount = m_joints.size();
if (jointCount == 0)
{
Error("Joint count is invalid or missing");
return false;
}
std::string line;
std::size_t count = 0;
do
{
if (!Advance())
return false;
std::size_t index = 0;
std::size_t size = m_currentLine.size();
do
{
float f;
int read;
if (std::sscanf(&m_currentLine[index], "%f%n", &f, &read) != 1)
{
UnrecognizedLine(true);
return false;
}
index += read;
m_animatedComponents[count] = f;
count++;
}
while (index < size);
}
while (count < animatedComponentsCount);
m_frames[m_frameIndex].joints.resize(jointCount);
for (std::size_t i = 0; i < jointCount; ++i)
{
Quaternionf jointOrient = m_joints[i].bindOrient;
Vector3f jointPos = m_joints[i].bindPos;
UInt32 j = 0;
if (m_joints[i].flags & 1) // Px
jointPos.x = m_animatedComponents[m_joints[i].index + j++];
if (m_joints[i].flags & 2) // Py
jointPos.y = m_animatedComponents[m_joints[i].index + j++];
if (m_joints[i].flags & 4) // Pz
jointPos.z = m_animatedComponents[m_joints[i].index + j++];
if (m_joints[i].flags & 8) // Qx
jointOrient.x = m_animatedComponents[m_joints[i].index + j++];
if (m_joints[i].flags & 16) // Qy
jointOrient.y = m_animatedComponents[m_joints[i].index + j++];
if (m_joints[i].flags & 32) // Qz
jointOrient.z = m_animatedComponents[m_joints[i].index + j++];
jointOrient.ComputeW();
m_frames[m_frameIndex].joints[i].orient = jointOrient;
m_frames[m_frameIndex].joints[i].pos = jointPos;
}
if (!Advance(false))
return true;
if (m_currentLine != "}")
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
Warning("Hierarchy braces closing not found");
#endif
// On tente de survivre à l'erreur
m_keepLastLine = true;
}
return true;
}
bool MD5AnimParser::ParseHierarchy()
{
std::size_t jointCount = m_joints.size();
if (jointCount == 0)
{
Error("Joint count is invalid or missing");
return false;
}
for (std::size_t i = 0; i < jointCount; ++i)
{
if (!Advance())
return false;
std::size_t pos = m_currentLine.find(' ');
if (pos == std::string::npos)
{
UnrecognizedLine(true);
return false;
}
if (pos >= 64)
{
NazaraError("Joint name is too long (>= 64 characters)");
return false;
}
char name[64];
if (std::sscanf(&m_currentLine[0], "%63s %d %u %u", &name[0], &m_joints[i].parent, &m_joints[i].flags, &m_joints[i].index) != 4)
{
UnrecognizedLine(true);
return false;
}
m_joints[i].name = Trim(name, '"');
Int32 parent = m_joints[i].parent;
if (parent >= 0)
{
if (static_cast<UInt32>(parent) >= jointCount)
{
Error("Joint's parent is out of bounds (" + NumberToString(parent) + " >= " + NumberToString(jointCount) + ')');
return false;
}
}
}
if (!Advance())
return false;
if (m_currentLine != "}")
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
Warning("Hierarchy braces closing not found");
#endif
// On tente de survivre à l'erreur
m_keepLastLine = true;
}
return true;
}
void MD5AnimParser::Warning(std::string_view message)
{
NazaraWarningFmt("{0} at line #{1}", message, m_lineCount);
}
void MD5AnimParser::UnrecognizedLine(bool error)
{
std::string message = "unrecognized \"" + m_currentLine + '"';
if (error)
Error(message);
else
Warning(message);
}
}

View File

@@ -0,0 +1,373 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MD5MeshLoader.hpp>
#include <Nazara/Core/IndexIterator.hpp>
#include <Nazara/Core/IndexMapper.hpp>
#include <Nazara/Core/Joint.hpp>
#include <Nazara/Core/MaterialData.hpp>
#include <Nazara/Core/Mesh.hpp>
#include <Nazara/Core/SkeletalMesh.hpp>
#include <Nazara/Core/Skeleton.hpp>
#include <Nazara/Core/StaticMesh.hpp>
#include <Nazara/Core/VertexMapper.hpp>
#include <Nazara/Core/Formats/MD5MeshParser.hpp>
#include <memory>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace
{
bool IsMD5MeshSupported(std::string_view extension)
{
return (extension == ".md5mesh");
}
Result<std::shared_ptr<Mesh>, ResourceLoadingError> LoadMD5Mesh(Stream& stream, const MeshParams& parameters)
{
MD5MeshParser parser(stream);
UInt64 streamPos = stream.GetCursorPos();
if (!parser.Check())
return Err(ResourceLoadingError::Unrecognized);
stream.SetCursorPos(streamPos);
if (!parser.Parse())
{
NazaraError("MD5Mesh parser failed");
return Err(ResourceLoadingError::DecodingError);
}
UInt32 maxWeightCount = 4;
if (auto result = parameters.custom.GetIntegerParameter("MaxWeightCount"))
{
maxWeightCount = SafeCast<UInt32>(result.GetValue());
if (maxWeightCount > 4)
{
NazaraWarning("MaxWeightCount cannot be over 4");
maxWeightCount = 4;
}
}
// Pour que le squelette soit correctement aligné, il faut appliquer un quaternion "de correction" aux joints à la base du squelette
Quaternionf rotationQuat = Quaternionf::RotationBetween(Vector3f::UnitX(), Vector3f::Forward()) *
Quaternionf::RotationBetween(Vector3f::UnitZ(), Vector3f::Up());
std::filesystem::path baseDir = stream.GetDirectory();
// Le hellknight de Doom 3 fait ~120 unités, et il est dit qu'il fait trois mètres
// Nous réduisons donc la taille générale des fichiers MD5 de 1/40
Matrix4f matrix = Matrix4f::Transform(Nz::Vector3f::Zero(), rotationQuat, Vector3f(1.f / 40.f));
rotationQuat = Quaternionf::Identity();
const MD5MeshParser::Joint* joints = parser.GetJoints();
const MD5MeshParser::Mesh* meshes = parser.GetMeshes();
UInt32 jointCount = parser.GetJointCount();
UInt32 meshCount = parser.GetMeshCount();
if (parameters.animated)
{
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
mesh->CreateSkeletal(jointCount);
Skeleton* skeleton = mesh->GetSkeleton();
for (UInt32 i = 0; i < jointCount; ++i)
{
Joint* joint = skeleton->GetJoint(i);
int parent = joints[i].parent;
if (parent >= 0)
joint->SetParent(skeleton->GetJoint(parent));
joint->SetInverseBindMatrix(Matrix4f::TransformInverse(joints[i].bindPos, joints[i].bindOrient));
joint->SetName(joints[i].name);
}
mesh->SetMaterialCount(meshCount);
for (UInt32 i = 0; i < meshCount; ++i)
{
const MD5MeshParser::Mesh& md5Mesh = meshes[i];
UInt32 indexCount = SafeCast<UInt32>(md5Mesh.triangles.size() * 3);
UInt32 vertexCount = SafeCast<UInt32>(md5Mesh.vertices.size());
bool largeIndices = (vertexCount > std::numeric_limits<UInt16>::max());
std::shared_ptr<IndexBuffer> indexBuffer = std::make_shared<IndexBuffer>((largeIndices) ? IndexType::U32 : IndexType::U16, indexCount, parameters.indexBufferFlags, parameters.bufferFactory);
std::shared_ptr<VertexBuffer> vertexBuffer = std::make_shared<VertexBuffer>(VertexDeclaration::Get(VertexLayout::XYZ_Normal_UV_Tangent_Skinning), UInt32(vertexCount), parameters.vertexBufferFlags, parameters.bufferFactory);
// Index buffer
IndexMapper indexMapper(*indexBuffer);
// Le format définit un set de triangles nous permettant de retrouver facilement les indices
// Cependant les sommets des triangles ne sont pas spécifiés dans le même ordre que ceux du moteur
// (On parle ici de winding)
UInt32 index = 0;
for (const MD5MeshParser::Triangle& triangle : md5Mesh.triangles)
{
// On les respécifie dans le bon ordre (inversion du winding)
indexMapper.Set(index++, triangle.x);
indexMapper.Set(index++, triangle.z);
indexMapper.Set(index++, triangle.y);
}
indexMapper.Unmap();
if (parameters.optimizeIndexBuffers)
indexBuffer->Optimize();
// Vertex buffer
struct Weight
{
float bias;
unsigned int jointIndex;
};
std::vector<Weight> tempWeights;
VertexMapper vertexMapper(*vertexBuffer);
auto posPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Position);
auto jointIndicesPtr = vertexMapper.GetComponentPtr<Vector4i32>(VertexComponent::JointIndices);
auto jointWeightPtr = vertexMapper.GetComponentPtr<Vector4f>(VertexComponent::JointWeights);
auto uvPtr = vertexMapper.GetComponentPtr<Vector2f>(VertexComponent::TexCoord);
for (const MD5MeshParser::Vertex& vertex : md5Mesh.vertices)
{
// Skinning MD5 (Formule d'Id Tech)
Vector3f finalPos(Vector3f::Zero());
// On stocke tous les poids dans le tableau temporaire en même temps qu'on calcule la position finale du sommet.
tempWeights.resize(vertex.weightCount);
for (unsigned int weightIndex = 0; weightIndex < vertex.weightCount; ++weightIndex)
{
const MD5MeshParser::Weight& weight = md5Mesh.weights[vertex.startWeight + weightIndex];
const MD5MeshParser::Joint& joint = joints[weight.joint];
finalPos += (joint.bindPos + joint.bindOrient * weight.pos) * weight.bias;
// Avant d'ajouter les poids, il faut s'assurer qu'il n'y en ait pas plus que le maximum supporté
// et dans le cas contraire, garder les poids les plus importants et les renormaliser
tempWeights[weightIndex] = {weight.bias, weight.joint};
}
// Avons nous plus de poids que le moteur ne peut en supporter ?
UInt32 weightCount = vertex.weightCount;
if (weightCount > maxWeightCount)
{
// Pour augmenter la qualité du skinning tout en ne gardant que X poids, on ne garde que les poids
// les plus importants, ayant le plus d'impact sur le sommet final
std::sort(tempWeights.begin(), tempWeights.end(), [] (const Weight& a, const Weight& b) -> bool {
return a.bias > b.bias;
});
// Sans oublier bien sûr de renormaliser les poids (que leur somme soit 1)
float weightSum = 0.f;
for (UInt32 j = 0; j < maxWeightCount; ++j)
weightSum += tempWeights[j].bias;
for (UInt32 j = 0; j < maxWeightCount; ++j)
tempWeights[j].bias /= weightSum;
weightCount = maxWeightCount;
}
if (posPtr)
*posPtr++ = finalPos;
if (uvPtr)
*uvPtr++ = Vector2f(parameters.texCoordOffset + vertex.uv * parameters.texCoordScale);
if (jointIndicesPtr)
{
Vector4i32& jointIndices = *jointIndicesPtr++;
for (UInt32 j = 0; j < maxWeightCount; ++j)
jointIndices[j] = (j < weightCount) ? tempWeights[j].jointIndex : 0;
}
if (jointWeightPtr)
{
Vector4f& jointWeights = *jointWeightPtr++;
for (UInt32 j = 0; j < maxWeightCount; ++j)
jointWeights[j] = (j < weightCount) ? tempWeights[j].bias : 0;
}
}
// Vertex colors (.md5mesh files have no vertex color)
if (auto colorPtr = vertexMapper.GetComponentPtr<Color>(VertexComponent::Color))
{
for (std::size_t j = 0; j < md5Mesh.vertices.size(); ++j)
*colorPtr++ = Color::White();
}
vertexMapper.Unmap();
// Material
ParameterList matData;
matData.SetParameter(MaterialData::BaseColorTexturePath, PathToString(baseDir / md5Mesh.shader));
matData.SetParameter(MaterialData::Type, "Phong");
mesh->SetMaterialData(i, std::move(matData));
// Submesh
std::shared_ptr<SkeletalMesh> subMesh = std::make_shared<SkeletalMesh>(vertexBuffer, indexBuffer);
if (parameters.vertexDeclaration->HasComponentOfType<Vector3f>(VertexComponent::Normal))
{
if (parameters.vertexDeclaration->HasComponentOfType<Vector3f>(VertexComponent::Tangent))
subMesh->GenerateNormalsAndTangents();
else
subMesh->GenerateNormals();
}
subMesh->SetMaterialIndex(i);
mesh->AddSubMesh(subMesh);
// Animation
// Il est peut-être éventuellement possible que la probabilité que l'animation ait le même nom soit non-nulle.
std::filesystem::path path = stream.GetPath();
if (!path.empty())
{
path.replace_extension(".md5anim");
if (std::filesystem::exists(path))
mesh->SetAnimation(path);
}
}
return mesh;
}
else
{
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
if (!mesh->CreateStatic()) // Ne devrait jamais échouer
{
NazaraInternalError("Failed to create mesh");
return Err(ResourceLoadingError::Internal);
}
mesh->SetMaterialCount(meshCount);
for (UInt32 i = 0; i < meshCount; ++i)
{
const MD5MeshParser::Mesh& md5Mesh = meshes[i];
UInt32 indexCount = SafeCast<UInt32>(md5Mesh.triangles.size() * 3);
UInt32 vertexCount = SafeCast<UInt32>(md5Mesh.vertices.size());
// Index buffer
bool largeIndices = (vertexCount > std::numeric_limits<UInt16>::max());
std::shared_ptr<IndexBuffer> indexBuffer = std::make_shared<IndexBuffer>((largeIndices) ? IndexType::U32 : IndexType::U16, indexCount, parameters.indexBufferFlags, parameters.bufferFactory);
IndexMapper indexMapper(*indexBuffer);
IndexIterator index = indexMapper.begin();
for (const MD5MeshParser::Triangle& triangle : md5Mesh.triangles)
{
// On les respécifie dans le bon ordre
*index++ = triangle.x;
*index++ = triangle.z;
*index++ = triangle.y;
}
indexMapper.Unmap();
if (parameters.optimizeIndexBuffers)
indexBuffer->Optimize();
// Vertex buffer
std::shared_ptr<VertexBuffer> vertexBuffer = std::make_shared<VertexBuffer>(parameters.vertexDeclaration, vertexCount, parameters.vertexBufferFlags, parameters.bufferFactory);
VertexMapper vertexMapper(*vertexBuffer);
// Vertex positions
if (auto posPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Position))
{
for (const MD5MeshParser::Vertex& md5Vertex : md5Mesh.vertices)
{
// Id Tech MD5 skinning
Vector3f finalPos(Vector3f::Zero());
for (unsigned int j = 0; j < md5Vertex.weightCount; ++j)
{
const MD5MeshParser::Weight& weight = md5Mesh.weights[md5Vertex.startWeight + j];
const MD5MeshParser::Joint& joint = joints[weight.joint];
finalPos += (joint.bindPos + joint.bindOrient * weight.pos) * weight.bias;
}
// On retourne le modèle dans le bon sens
*posPtr++ = matrix * finalPos;
}
}
// Vertex UVs
if (auto uvPtr = vertexMapper.GetComponentPtr<Vector2f>(VertexComponent::TexCoord))
{
for (const MD5MeshParser::Vertex& md5Vertex : md5Mesh.vertices)
*uvPtr++ = parameters.texCoordOffset + md5Vertex.uv * parameters.texCoordScale;
}
// Vertex colors (.md5mesh files have no vertex color)
if (auto colorPtr = vertexMapper.GetComponentPtr<Color>(VertexComponent::Color))
{
for (std::size_t j = 0; j < md5Mesh.vertices.size(); ++j)
*colorPtr++ = Color::White();
}
vertexMapper.Unmap();
// Submesh
std::shared_ptr<StaticMesh> subMesh = std::make_shared<StaticMesh>(vertexBuffer, indexBuffer);
subMesh->GenerateAABB();
subMesh->SetMaterialIndex(i);
if (parameters.vertexDeclaration->HasComponentOfType<Vector3f>(VertexComponent::Normal))
{
if (parameters.vertexDeclaration->HasComponentOfType<Vector3f>(VertexComponent::Tangent))
subMesh->GenerateNormalsAndTangents();
else
subMesh->GenerateNormals();
}
mesh->AddSubMesh(subMesh);
// Material
ParameterList matData;
matData.SetParameter(MaterialData::BaseColorTexturePath, PathToString(baseDir / md5Mesh.shader));
mesh->SetMaterialData(i, std::move(matData));
}
if (parameters.center)
mesh->Recenter();
return mesh;
}
}
}
namespace Loaders
{
MeshLoader::Entry GetMeshLoader_MD5Mesh()
{
MeshLoader::Entry loader;
loader.extensionSupport = IsMD5MeshSupported;
loader.streamLoader = LoadMD5Mesh;
loader.parameterFilter = [](const MeshParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinMD5MeshLoader"); result.GetValueOr(false))
return false;
return true;
};
return loader;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_MD5MESHLOADER_HPP
#define NAZARA_CORE_FORMATS_MD5MESHLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Mesh.hpp>
namespace Nz::Loaders
{
MeshLoader::Entry GetMeshLoader_MD5Mesh();
}
#endif // NAZARA_CORE_FORMATS_MD5MESHLOADER_HPP

View File

@@ -0,0 +1,462 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MD5MeshParser.hpp>
#include <Nazara/Core/Config.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Stream.hpp>
#include <Nazara/Core/StringExt.hpp>
#include <cstdio>
#include <memory>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
MD5MeshParser::MD5MeshParser(Stream& stream) :
m_stream(stream),
m_streamFlags(stream.GetStreamOptions()), //< Saves stream flags
m_keepLastLine(false),
m_lineCount(0),
m_meshIndex(0)
{
m_stream.EnableTextMode(true);
}
MD5MeshParser::~MD5MeshParser()
{
// Reset stream flags
if ((m_streamFlags & StreamOption::Text) == 0)
m_stream.EnableTextMode(false);
}
bool MD5MeshParser::Check()
{
if (Advance(false))
{
unsigned int version;
if (std::sscanf(&m_currentLine[0], " MD5Version %u", &version) == 1)
{
if (version == 10)
return true;
}
}
return false;
}
const MD5MeshParser::Joint* MD5MeshParser::GetJoints() const
{
return m_joints.data();
}
UInt32 MD5MeshParser::GetJointCount() const
{
return static_cast<UInt32>(m_joints.size());
}
const MD5MeshParser::Mesh* MD5MeshParser::GetMeshes() const
{
return m_meshes.data();
}
UInt32 MD5MeshParser::GetMeshCount() const
{
return static_cast<UInt32>(m_meshes.size());
}
bool MD5MeshParser::Parse()
{
while (Advance(false))
{
switch (m_currentLine[0])
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
case 'M': // MD5Version
if (!StartsWith(m_currentLine, "MD5Version "))
UnrecognizedLine();
break;
case 'c': // commandline
if (!StartsWith(m_currentLine, "commandline "))
UnrecognizedLine();
break;
#endif
case 'j': // joints
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!StartsWith(m_currentLine, "joints {"))
{
UnrecognizedLine();
break;
}
#endif
if (!ParseJoints())
{
Error("Failed to parse joints");
return false;
}
break;
case 'm': // mesh
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!StartsWith(m_currentLine, "mesh {"))
{
UnrecognizedLine();
break;
}
#endif
if (m_meshIndex >= m_meshes.size())
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
Warning("More meshes than registred");
#endif
m_meshes.emplace_back();
}
if (!ParseMesh())
{
NazaraError("failed to parse mesh");
return false;
}
m_meshIndex++;
break;
}
case 'n': // num[Frames/Joints]
{
unsigned int count;
if (std::sscanf(&m_currentLine[0], "numJoints %u", &count) == 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!m_joints.empty())
Warning("Joint count is already defined");
#endif
m_joints.resize(count);
}
else if (std::sscanf(&m_currentLine[0], "numMeshes %u", &count) == 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!m_meshes.empty())
Warning("Mesh count is already defined");
#endif
m_meshes.resize(count);
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
break;
}
default:
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
break;
}
}
return true;
}
bool MD5MeshParser::Advance(bool required)
{
if (!m_keepLastLine)
{
do
{
if (m_stream.EndOfStream())
{
if (required)
Error("Incomplete MD5 file");
return false;
}
m_lineCount++;
m_currentLine = m_stream.ReadLine();
if (std::size_t p = m_currentLine.find("//"); p != m_currentLine.npos)
{
if (p > 0)
m_currentLine = m_currentLine.substr(0, p - 1);
else
m_currentLine.clear();
}
// Trim left
m_currentLine.erase(m_currentLine.begin(), std::find_if(m_currentLine.begin(), m_currentLine.end(), [](char c)
{
return !std::isspace(c);
}));
if (m_currentLine.empty())
continue;
}
while (m_currentLine.empty());
}
else
m_keepLastLine = false;
return true;
}
void MD5MeshParser::Error(std::string_view message)
{
NazaraErrorFmt("{0} on line #{1}", message, m_lineCount);
}
bool MD5MeshParser::ParseJoints()
{
std::size_t jointCount = m_joints.size();
if (jointCount == 0)
{
Error("Joint count is invalid or missing");
return false;
}
for (std::size_t i = 0; i < jointCount; ++i)
{
if (!Advance())
return false;
std::size_t pos = m_currentLine.find(' ');
if (pos == std::string::npos)
{
UnrecognizedLine(true);
return false;
}
if (pos >= 64)
{
NazaraError("joint name is too long (>= 64 characters)");
return false;
}
char name[64];
if (std::sscanf(&m_currentLine[0], "%63s %d ( %f %f %f ) ( %f %f %f )", &name[0], &m_joints[i].parent,
&m_joints[i].bindPos.x, &m_joints[i].bindPos.y, &m_joints[i].bindPos.z,
&m_joints[i].bindOrient.x, &m_joints[i].bindOrient.y, &m_joints[i].bindOrient.z) != 8)
{
UnrecognizedLine(true);
return false;
}
m_joints[i].name = Trim(name, '"');
Int32 parent = m_joints[i].parent;
if (parent >= 0)
{
if (static_cast<std::size_t>(parent) >= jointCount)
{
Error("Joint's parent is out of bounds (" + std::to_string(parent) + " >= " + std::to_string(jointCount) + ')');
return false;
}
}
m_joints[i].bindOrient.ComputeW(); // On calcule la composante W
}
if (!Advance())
return false;
if (m_currentLine != "}")
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
Warning("Hierarchy braces closing not found");
#endif
// On tente de survivre à l'erreur
m_keepLastLine = true;
}
return true;
}
bool MD5MeshParser::ParseMesh()
{
bool finished = false;
while (!finished && Advance(false))
{
switch (m_currentLine[0])
{
case '}':
finished = true;
break;
case 's': // shader
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!StartsWith(m_currentLine, "shader "))
{
UnrecognizedLine();
break;
}
#endif
std::string_view shader = m_currentLine;
shader = shader.substr(7);
if (shader.empty())
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
break;
}
if (shader.empty())
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
break;
}
if (shader.front() == '"' && shader.back() == '"')
{
shader.remove_prefix(1);
shader.remove_suffix(1);
}
m_meshes[m_meshIndex].shader = shader;
break;
}
case 'n': // num[tris/verts]
{
unsigned int count;
if (std::sscanf(&m_currentLine[0], "numtris %u", &count) == 1)
{
m_meshes[m_meshIndex].triangles.resize(count);
for (unsigned int i = 0; i < count; ++i)
{
if (!Advance())
return false;
Triangle& triangle = m_meshes[m_meshIndex].triangles[i];
unsigned int index;
if (std::sscanf(&m_currentLine[0], "tri %u %u %u %u", &index, &triangle.x, &triangle.y, &triangle.z) != 4)
{
UnrecognizedLine(true);
return false;
}
if (index != i)
{
Error("Unexpected triangle index (expected " + std::to_string(i) + ", got " + std::to_string(index) + ')');
return false;
}
}
}
else if (std::sscanf(&m_currentLine[0], "numverts %u", &count) == 1)
{
m_meshes[m_meshIndex].vertices.resize(count);
for (unsigned int i = 0; i < count; ++i)
{
if (!Advance())
return false;
Vertex& vertex = m_meshes[m_meshIndex].vertices[i];
unsigned int index;
if (std::sscanf(&m_currentLine[0], "vert %u ( %f %f ) %u %u", &index, &vertex.uv.x, &vertex.uv.y, &vertex.startWeight, &vertex.weightCount) != 5)
{
UnrecognizedLine(true);
return false;
}
if (index != i)
{
Error("Unexpected vertex index (expected " + std::to_string(i) + ", got " + std::to_string(index) + ')');
return false;
}
}
}
else if (std::sscanf(&m_currentLine[0], "numweights %u", &count) == 1)
{
m_meshes[m_meshIndex].weights.resize(count);
for (unsigned int i = 0; i < count; ++i)
{
if (!Advance())
return false;
Weight& weight = m_meshes[m_meshIndex].weights[i];
unsigned int index;
if (std::sscanf(&m_currentLine[0], "weight %u %u %f ( %f %f %f )", &index, &weight.joint, &weight.bias,
&weight.pos.x, &weight.pos.y, &weight.pos.z) != 6)
{
UnrecognizedLine(true);
return false;
}
if (index != i)
{
Error("Unexpected weight index (expected " + std::to_string(i) + ", got " + std::to_string(index) + ')');
return false;
}
}
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
break;
}
default:
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
break;
}
}
if (m_meshes[m_meshIndex].triangles.empty())
{
NazaraError("mesh has no triangles");
return false;
}
if (m_meshes[m_meshIndex].vertices.empty())
{
NazaraError("mesh has no vertices");
return false;
}
if (m_meshes[m_meshIndex].weights.empty())
{
NazaraError("mesh has no weights");
return false;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!finished)
Warning("Mesh braces closing not found");
#endif
return true;
}
void MD5MeshParser::Warning(std::string_view message)
{
NazaraWarningFmt("{0} on line #{1}", message, m_lineCount);
}
void MD5MeshParser::UnrecognizedLine(bool error)
{
std::string message = "unrecognized \"" + m_currentLine + '"';
if (error)
Error(message);
else
Warning(message);
}
}

View File

@@ -0,0 +1,660 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/MTLParser.hpp>
#include <Nazara/Core/Config.hpp>
#include <Nazara/Core/StringExt.hpp>
#include <NazaraUtils/CallOnExit.hpp>
#include <cstdio>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace
{
template<std::size_t N>
bool TestKeyword(std::string_view currentLine, const char(&keyword)[N], std::size_t& offset)
{
if (currentLine.size() > N && StartsWith(currentLine, keyword, CaseIndependent{}) && std::isspace(currentLine[N - 1]))
{
offset = N;
while (offset < currentLine.size() && std::isspace(currentLine[offset]))
offset++;
return offset < currentLine.size();
}
else
return false;
}
}
bool MTLParser::Parse(Stream& stream)
{
m_currentStream = &stream;
// force stream in text mode, reset it at the end
CallOnExit resetTextMode([&stream]
{
stream.EnableTextMode(false);
});
if ((stream.GetStreamOptions() & StreamOption::Text) == 0)
stream.EnableTextMode(true);
else
resetTextMode.Reset();
m_keepLastLine = false;
m_lineCount = 0;
m_materials.clear();
Material* currentMaterial = nullptr;
std::size_t offset;
while (Advance(false))
{
switch (std::tolower(m_currentLine[0]))
{
case 'b':
{
if (TestKeyword(m_currentLine, "bump", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->bumpMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'd':
{
if (TestKeyword(m_currentLine, "d", offset))
{
float alpha;
if (std::sscanf(&m_currentLine[2], "%f", &alpha) == 1)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->alpha = alpha;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "decal", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->decalMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "disp", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->displacementMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'e':
{
if (TestKeyword(m_currentLine, "emissive", offset))
{
// <!> This is a custom keyword
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->emissiveMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'k':
{
if (TestKeyword(m_currentLine, "ka", offset))
{
float r, g, b;
if (std::sscanf(&m_currentLine[offset], "%f %f %f", &r, &g, &b) == 3)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->ambient = Color(r, g, b);
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "kd", offset))
{
float r, g, b;
if (std::sscanf(&m_currentLine[offset], "%f %f %f", &r, &g, &b) == 3)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->diffuse = Color(r, g, b);
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "ks", offset))
{
float r, g, b;
if (std::sscanf(&m_currentLine[offset], "%f %f %f", &r, &g, &b) == 3)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->specular = Color(r, g, b);
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'i':
{
if (TestKeyword(m_currentLine, "illum", offset))
{
unsigned int model;
if (std::sscanf(&m_currentLine[offset], "%u", &model) == 1)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->illumModel = model;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'm':
{
if (TestKeyword(m_currentLine, "map_ka", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->ambientMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_kd", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->diffuseMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_ks", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->specularMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_bump", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->bumpMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_d", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->alphaMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_decal", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->decalMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_disp", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->displacementMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_refl", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->reflectionMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_normal", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->normalMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "map_emissive", offset))
{
// <!> This is a custom keyword
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->emissiveMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'n':
{
if (TestKeyword(m_currentLine, "ni", offset))
{
float density;
if (std::sscanf(&m_currentLine[offset], "%f", &density) == 1)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->refractionIndex = density;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "ns", offset))
{
float coef;
if (std::sscanf(&m_currentLine[offset], "%f", &coef) == 1)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->shininess = coef;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "normal", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->normalMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
else if (TestKeyword(m_currentLine, "newmtl", offset))
{
std::string materialName = m_currentLine.substr(offset);
if (!materialName.empty())
currentMaterial = AddMaterial(materialName);
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 'r':
{
if (TestKeyword(m_currentLine, "refl", offset))
{
std::string map = m_currentLine.substr(offset);
if (!map.empty())
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->reflectionMap = map;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
case 't':
{
if (TestKeyword(m_currentLine, "tr", offset))
{
float alpha;
if (std::sscanf(&m_currentLine[offset], "%f", &alpha) == 1)
{
if (!currentMaterial)
currentMaterial = AddMaterial("default");
currentMaterial->alpha = 1.f - alpha; // tr vaut pour la "valeur de transparence", 0 = opaque
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else
UnrecognizedLine();
#endif
}
break;
}
default:
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
UnrecognizedLine();
#endif
break;
}
}
return true;
}
bool MTLParser::Save(Stream& stream) const
{
m_currentStream = &stream;
// force stream in text mode, reset it at the end
CallOnExit resetTextMode([&stream]
{
stream.EnableTextMode(false);
});
if ((stream.GetStreamOptions() & StreamOption::Text) == 0)
stream.EnableTextMode(true);
else
resetTextMode.Reset();
m_outputStream.str({});
EmitLine("# Exported by Nazara Engine");
EmitLine();
Emit("# material count: ");
Emit(m_materials.size());
EmitLine();
for (auto& pair : m_materials)
{
const std::string& matName = pair.first;
const Material& mat = pair.second;
Emit("newmtl ");
EmitLine(matName);
EmitLine();
Emit("Ka ");
Emit(mat.ambient.r);
Emit(' ');
Emit(mat.ambient.g);
Emit(' ');
Emit(mat.ambient.b);
EmitLine();
Emit("Kd ");
Emit(mat.diffuse.r);
Emit(' ');
Emit(mat.diffuse.g);
Emit(' ');
Emit(mat.diffuse.b);
EmitLine();
Emit("Ks ");
Emit(mat.specular.r);
Emit(' ');
Emit(mat.specular.g);
Emit(' ');
Emit(mat.specular.b);
EmitLine();
if (!NumberEquals(mat.alpha, 1.f))
{
Emit("d ");
EmitLine(mat.alpha);
}
if (!NumberEquals(mat.refractionIndex, 1.f))
{
Emit("ni ");
EmitLine(mat.refractionIndex);
}
if (!NumberEquals(mat.shininess, 1.f))
{
Emit("ns ");
EmitLine(mat.shininess);
}
if (mat.illumModel != 0)
{
Emit("illum ");
EmitLine(mat.illumModel);
}
if (!mat.ambientMap.empty())
{
Emit("map_Ka ");
EmitLine(mat.ambientMap);
}
if (!mat.diffuseMap.empty())
{
Emit("map_Kd ");
EmitLine(mat.diffuseMap);
}
if (!mat.specularMap.empty())
{
Emit("map_Ks ");
EmitLine(mat.specularMap);
}
if (!mat.bumpMap.empty())
{
Emit("map_bump ");
EmitLine(mat.bumpMap);
}
if (!mat.alphaMap.empty())
{
Emit("map_d ");
EmitLine(mat.alphaMap);
}
if (!mat.decalMap.empty())
{
Emit("map_decal ");
EmitLine(mat.decalMap);
}
if (!mat.displacementMap.empty())
{
Emit("map_disp ");
EmitLine(mat.displacementMap);
}
if (!mat.reflectionMap.empty())
{
Emit("map_refl ");
EmitLine(mat.reflectionMap);
}
EmitLine();
}
Flush();
return true;
}
bool MTLParser::Advance(bool required)
{
if (!m_keepLastLine)
{
do
{
if (m_currentStream->EndOfStream())
{
if (required)
Error("Incomplete MTL file");
return false;
}
m_lineCount++;
m_currentLine = m_currentStream->ReadLine();
if (std::size_t p = m_currentLine.find('#'); p != m_currentLine.npos)
{
if (p > 0)
m_currentLine = m_currentLine.substr(0, p - 1);
else
m_currentLine.clear();
}
m_currentLine = Trim(m_currentLine);
if (m_currentLine.empty())
continue;
}
while (m_currentLine.empty());
}
else
m_keepLastLine = false;
return true;
}
}

View File

@@ -0,0 +1,371 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/OBJLoader.hpp>
#include <Nazara/Core/ErrorFlags.hpp>
#include <Nazara/Core/IndexMapper.hpp>
#include <Nazara/Core/MaterialData.hpp>
#include <Nazara/Core/Mesh.hpp>
#include <Nazara/Core/StaticMesh.hpp>
#include <Nazara/Core/VertexMapper.hpp>
#include <Nazara/Core/Formats/MTLParser.hpp>
#include <Nazara/Core/Formats/OBJParser.hpp>
#include <limits>
#include <memory>
#include <unordered_map>
#include <Nazara/Core/Debug.hpp>
// TODO: Use only one index buffer / vertex buffer for all submeshes
namespace Nz
{
namespace
{
bool IsOBJSupported(std::string_view extension)
{
return (extension == ".obj");
}
bool ParseMTL(Mesh& mesh, const std::filesystem::path& filePath, const std::string* materials, const OBJParser::Mesh* meshes, std::size_t meshCount)
{
File file(filePath);
if (!file.Open(OpenMode::Read | OpenMode::Text))
{
NazaraErrorFmt("failed to open MTL file ({0})", file.GetPath());
return false;
}
MTLParser materialParser;
if (!materialParser.Parse(file))
{
NazaraError("MTL parser failed");
return false;
}
std::unordered_map<std::string, ParameterList> materialCache;
std::filesystem::path baseDir = file.GetDirectory();
for (std::size_t i = 0; i < meshCount; ++i)
{
const std::string& matName = materials[meshes[i].material];
const MTLParser::Material* mtlMat = materialParser.GetMaterial(matName);
if (!mtlMat)
{
NazaraWarningFmt("MTL has no material \"{0}\"", matName);
continue;
}
auto it = materialCache.find(matName);
if (it == materialCache.end())
{
ParameterList data;
float alphaValue = mtlMat->alpha;
Color ambientColor(mtlMat->ambient);
Color baseColor(mtlMat->diffuse);
Color specularColor(mtlMat->specular);
ambientColor.a = alphaValue;
baseColor.a = alphaValue;
specularColor.a = alphaValue;
data.SetParameter(MaterialData::Type, "Phong");
data.SetParameter(MaterialData::AmbientColor, ambientColor);
data.SetParameter(MaterialData::BaseColor, baseColor);
data.SetParameter(MaterialData::Shininess, mtlMat->shininess);
data.SetParameter(MaterialData::SpecularColor, specularColor);
if (!mtlMat->alphaMap.empty())
{
std::filesystem::path fullPath = mtlMat->alphaMap;
if (!fullPath.is_absolute())
fullPath = baseDir / fullPath;
data.SetParameter(MaterialData::AlphaTexturePath, PathToString(fullPath));
}
if (!mtlMat->diffuseMap.empty())
{
std::filesystem::path fullPath = mtlMat->diffuseMap;
if (!fullPath.is_absolute())
fullPath = baseDir / fullPath;
data.SetParameter(MaterialData::BaseColorTexturePath, PathToString(fullPath));
}
if (!mtlMat->emissiveMap.empty())
{
std::filesystem::path fullPath = mtlMat->emissiveMap;
if (!fullPath.is_absolute())
fullPath = baseDir / fullPath;
data.SetParameter(MaterialData::EmissiveTexturePath, PathToString(fullPath));
}
if (!mtlMat->normalMap.empty())
{
std::filesystem::path fullPath = mtlMat->normalMap;
if (!fullPath.is_absolute())
fullPath = baseDir / fullPath;
data.SetParameter(MaterialData::NormalTexturePath, PathToString(fullPath));
}
if (!mtlMat->specularMap.empty())
{
std::filesystem::path fullPath = mtlMat->specularMap;
if (!fullPath.is_absolute())
fullPath = baseDir / fullPath;
data.SetParameter(MaterialData::SpecularTexturePath, PathToString(fullPath));
}
// If we either have an alpha value or an alpha map, let's configure the material for transparency
if (alphaValue != 255 || !mtlMat->alphaMap.empty())
{
// Some default settings
data.SetParameter(MaterialData::Blending, true);
data.SetParameter(MaterialData::DepthWrite, true);
data.SetParameter(MaterialData::BlendDstAlpha, static_cast<long long>(BlendFunc::Zero));
data.SetParameter(MaterialData::BlendDstColor, static_cast<long long>(BlendFunc::InvSrcAlpha));
data.SetParameter(MaterialData::BlendModeAlpha, static_cast<long long>(BlendEquation::Add));
data.SetParameter(MaterialData::BlendModeColor, static_cast<long long>(BlendEquation::Add));
data.SetParameter(MaterialData::BlendSrcAlpha, static_cast<long long>(BlendFunc::One));
data.SetParameter(MaterialData::BlendSrcColor, static_cast<long long>(BlendFunc::SrcAlpha));
}
it = materialCache.emplace(matName, std::move(data)).first;
}
mesh.SetMaterialData(meshes[i].material, it->second);
}
return true;
}
Result<std::shared_ptr<Mesh>, ResourceLoadingError> LoadOBJ(Stream& stream, const MeshParams& parameters)
{
long long reservedVertexCount = parameters.custom.GetIntegerParameter("ReserveVertexCount").GetValueOr(1'000);
OBJParser parser;
UInt64 streamPos = stream.GetCursorPos();
if (!parser.Check(stream))
return Err(ResourceLoadingError::Unrecognized);
stream.SetCursorPos(streamPos);
if (!parser.Parse(stream, reservedVertexCount))
{
NazaraError("OBJ parser failed");
return Err(ResourceLoadingError::DecodingError);
}
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
mesh->CreateStatic();
const std::string* materials = parser.GetMaterials();
const Vector4f* positions = parser.GetPositions();
const Vector3f* normals = parser.GetNormals();
const Vector3f* texCoords = parser.GetTexCoords();
const OBJParser::Mesh* meshes = parser.GetMeshes();
std::size_t meshCount = parser.GetMeshCount();
NazaraAssert(materials != nullptr && positions != nullptr && normals != nullptr && texCoords != nullptr && meshes != nullptr && meshCount > 0,
"Invalid OBJParser output");
// Triangulation temporary vector
std::vector<UInt32> faceIndices;
for (std::size_t i = 0; i < meshCount; ++i)
{
std::size_t faceCount = meshes[i].faces.size();
if (faceCount == 0)
continue;
std::vector<UInt32> indices;
indices.reserve(faceCount*3); // Pire cas si les faces sont des triangles
// Afin d'utiliser OBJParser::FaceVertex comme clé dans un unordered_map,
// nous devons fournir un foncteur de hash ainsi qu'un foncteur de comparaison
// Hash
struct FaceVertexHasher
{
std::size_t operator()(const OBJParser::FaceVertex& o) const
{
std::size_t seed = 0;
HashCombine(seed, o.normal);
HashCombine(seed, o.position);
HashCombine(seed, o.texCoord);
return seed;
}
};
// Comparaison
struct FaceVertexComparator
{
bool operator()(const OBJParser::FaceVertex& lhs, const OBJParser::FaceVertex& rhs) const
{
return lhs.normal == rhs.normal &&
lhs.position == rhs.position &&
lhs.texCoord == rhs.texCoord;
}
};
std::unordered_map<OBJParser::FaceVertex, unsigned int, FaceVertexHasher, FaceVertexComparator> vertices;
vertices.reserve(meshes[i].vertices.size());
UInt32 vertexCount = 0;
for (unsigned int j = 0; j < faceCount; ++j)
{
std::size_t faceVertexCount = meshes[i].faces[j].vertexCount;
faceIndices.resize(faceVertexCount);
for (std::size_t k = 0; k < faceVertexCount; ++k)
{
const OBJParser::FaceVertex& vertex = meshes[i].vertices[meshes[i].faces[j].firstVertex + k];
auto it = vertices.find(vertex);
if (it == vertices.end())
it = vertices.emplace(vertex, vertexCount++).first;
faceIndices[k] = it->second;
}
// Triangulation
for (std::size_t k = 1; k < faceVertexCount-1; ++k)
{
indices.push_back(faceIndices[0]);
indices.push_back(faceIndices[k]);
indices.push_back(faceIndices[k+1]);
}
}
// Création des buffers
bool largeIndices = (vertexCount > std::numeric_limits<UInt16>::max());
std::shared_ptr<IndexBuffer> indexBuffer = std::make_shared<IndexBuffer>((largeIndices) ? IndexType::U32 : IndexType::U16, SafeCast<UInt32>(indices.size()), parameters.indexBufferFlags, parameters.bufferFactory);
std::shared_ptr<VertexBuffer> vertexBuffer = std::make_shared<VertexBuffer>(parameters.vertexDeclaration, vertexCount, parameters.vertexBufferFlags, parameters.bufferFactory);
// Remplissage des indices
IndexMapper indexMapper(*indexBuffer);
for (std::size_t j = 0; j < indices.size(); ++j)
indexMapper.Set(j, indices[j]);
indexMapper.Unmap(); // Pour laisser les autres tâches affecter l'index buffer
if (parameters.optimizeIndexBuffers)
indexBuffer->Optimize();
// Remplissage des vertices
bool hasNormals = true;
bool hasTexCoords = true;
VertexMapper vertexMapper(*vertexBuffer);
auto normalPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Normal);
auto posPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Position);
auto uvPtr = vertexMapper.GetComponentPtr<Vector2f>(VertexComponent::TexCoord);
if (!normalPtr)
hasNormals = false;
if (!uvPtr)
hasTexCoords = false;
for (auto& vertexPair : vertices)
{
const OBJParser::FaceVertex& vertexIndices = vertexPair.first;
unsigned int index = vertexPair.second;
if (posPtr)
{
const Vector4f& vec = positions[vertexIndices.position - 1];
posPtr[index] = TransformPositionTRS(parameters.vertexOffset, parameters.vertexRotation, parameters.vertexScale, Vector3f(vec));
}
if (hasNormals)
{
if (vertexIndices.normal > 0)
normalPtr[index] = TransformNormalTRS(parameters.vertexRotation, parameters.vertexScale, normals[vertexIndices.normal - 1]);
else
hasNormals = false;
}
if (hasTexCoords)
{
if (vertexIndices.texCoord > 0)
{
Vector2f uv = Vector2f(texCoords[vertexIndices.texCoord - 1]);
uv.y = 1.f - uv.y; //< OBJ model texcoords seems to majority start from bottom left
uvPtr[index] = Vector2f(parameters.texCoordOffset + uv * parameters.texCoordScale);
}
else
hasTexCoords = false;
}
}
// Official .obj files have no vertex color, fill it with white
if (auto colorPtr = vertexMapper.GetComponentPtr<Color>(VertexComponent::Color))
{
for (UInt32 j = 0; j < vertexCount; ++j)
colorPtr[j] = Color::White();
}
vertexMapper.Unmap();
std::shared_ptr<StaticMesh> subMesh = std::make_shared<StaticMesh>(std::move(vertexBuffer), indexBuffer);
subMesh->GenerateAABB();
subMesh->SetMaterialIndex(meshes[i].material);
// Ce que nous pouvons générer dépend des données à disposition (par exemple les tangentes nécessitent des coordonnées de texture)
if (hasNormals && hasTexCoords)
subMesh->GenerateTangents();
else if (hasTexCoords)
subMesh->GenerateNormalsAndTangents();
else if (normalPtr)
subMesh->GenerateNormals();
mesh->AddSubMesh(meshes[i].name + '_' + materials[meshes[i].material], subMesh);
}
mesh->SetMaterialCount(parser.GetMaterialCount());
if (parameters.center)
mesh->Recenter();
// On charge les matériaux si demandé
std::filesystem::path mtlLib = parser.GetMtlLib();
if (!mtlLib.empty())
{
ErrorFlags errFlags({}, ~ErrorMode::ThrowException);
ParseMTL(*mesh, stream.GetDirectory() / mtlLib, materials, meshes, meshCount);
}
return mesh;
}
}
namespace Loaders
{
MeshLoader::Entry GetMeshLoader_OBJ()
{
MeshLoader::Entry loader;
loader.extensionSupport = IsOBJSupported;
loader.streamLoader = LoadOBJ;
loader.parameterFilter = [](const MeshParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinOBJLoader"); result.GetValueOr(false))
return false;
return true;
};
return loader;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_OBJLOADER_HPP
#define NAZARA_CORE_FORMATS_OBJLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Mesh.hpp>
namespace Nz::Loaders
{
MeshLoader::Entry GetMeshLoader_OBJ();
}
#endif // NAZARA_CORE_FORMATS_OBJLOADER_HPP

View File

@@ -0,0 +1,679 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/OBJParser.hpp>
#include <Nazara/Core/Config.hpp>
#include <Nazara/Core/Stream.hpp>
#include <Nazara/Core/StringExt.hpp>
#include <NazaraUtils/CallOnExit.hpp>
#include <NazaraUtils/PathUtils.hpp>
#include <tsl/ordered_map.h>
#include <cctype>
#include <memory>
#include <unordered_map>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
bool OBJParser::Check(Stream& stream)
{
m_currentStream = &stream;
m_errorCount = 0;
m_keepLastLine = false;
m_lineCount = 0;
// force stream in text mode, reset it at the end
CallOnExit resetTextMode([&stream]
{
stream.EnableTextMode(false);
});
if ((stream.GetStreamOptions() & StreamOption::Text) == 0)
stream.EnableTextMode(true);
else
resetTextMode.Reset();
unsigned int failureCount = 0;
while (Advance(false))
{
switch (std::tolower(m_currentLine[0]))
{
case '#': //< Comment
failureCount--;
break;
case 'f': //< Face
case 'g': //< Group (inside a mesh)
case 'o': //< Object (defines a mesh)
case 's': //< Smooth
{
if (m_currentLine.size() > 1 && m_currentLine[1] == ' ')
return true;
break;
}
case 'm': //< MTLLib
if (StartsWith(m_currentLine, "mtllib "))
return true;
break;
case 'u': //< Usemtl
if (StartsWith(m_currentLine, "usemtl "))
return true;
break;
case 'v': //< Position/Normal/Texcoords
{
if (StartsWith(m_currentLine, "v ") ||
StartsWith(m_currentLine, "vn ") ||
StartsWith(m_currentLine, "vt "))
return true;
break;
}
default:
break;
}
if (++failureCount > 20U)
return false;
}
return false;
}
bool OBJParser::Parse(Nz::Stream& stream, std::size_t reservedVertexCount)
{
m_currentStream = &stream;
m_errorCount = 0;
m_keepLastLine = false;
m_lineCount = 0;
// force stream in text mode, reset it at the end
CallOnExit resetTextMode([&stream]
{
stream.EnableTextMode(false);
});
if ((stream.GetStreamOptions() & StreamOption::Text) == 0)
stream.EnableTextMode(true);
else
resetTextMode.Reset();
std::string matName, meshName;
matName = meshName = "default";
m_meshes.clear();
m_mtlLib.clear();
m_normals.clear();
m_positions.clear();
m_texCoords.clear();
// Reserve some space for incoming vertices
m_normals.reserve(reservedVertexCount);
m_positions.reserve(reservedVertexCount);
m_texCoords.reserve(reservedVertexCount);
// Sort meshes by material and group
using MatPair = std::pair<Mesh, unsigned int>;
tsl::ordered_map<std::string, tsl::ordered_map<std::string, MatPair>> meshesByName;
UInt32 faceReserve = 0;
UInt32 vertexReserve = 0;
unsigned int matCount = 0;
auto GetMaterial = [&] (const std::string& mesh, const std::string& mat) -> Mesh*
{
auto& map = meshesByName[mesh];
auto it = map.find(mat);
if (it == map.end())
it = map.insert(std::make_pair(mat, MatPair(Mesh(), matCount++))).first;
Mesh& meshData = it.value().first;
meshData.faces.reserve(faceReserve);
meshData.vertices.reserve(vertexReserve);
faceReserve = 0;
vertexReserve = 0;
return &meshData;
};
// On prépare le mesh par défaut
Mesh* currentMesh = nullptr;
while (Advance(false))
{
switch (std::tolower(m_currentLine[0]))
{
case '#': //< Comment
// Some softwares write comments to gives the number of vertex/faces an importer can expect
unsigned int data;
if (std::sscanf(m_currentLine.data(), "# position count: %u", &data) == 1)
m_positions.reserve(data);
else if (std::sscanf(m_currentLine.data(), "# normal count: %u", &data) == 1)
m_normals.reserve(data);
else if (std::sscanf(m_currentLine.data(), "# texcoords count: %u", &data) == 1)
m_texCoords.reserve(data);
else if (std::sscanf(m_currentLine.data(), "# face count: %u", &data) == 1)
faceReserve = data;
else if (std::sscanf(m_currentLine.data(), "# vertex count: %u", &data) == 1)
vertexReserve = data;
break;
case 'f': //< Face
{
if (m_currentLine.size() < 7) // Since we only treat triangles, this is the minimum length of a face line (f 1 2 3)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
std::size_t vertexCount = std::count(m_currentLine.begin(), m_currentLine.end(), ' ');
if (vertexCount < 3)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
if (!currentMesh)
currentMesh = GetMaterial(meshName, matName);
Face face;
face.firstVertex = currentMesh->vertices.size();
face.vertexCount = vertexCount;
currentMesh->vertices.resize(face.firstVertex + vertexCount, FaceVertex{0, 0, 0});
bool error = false;
unsigned int pos = 2;
for (unsigned int i = 0; i < vertexCount; ++i)
{
int offset;
int n = 0;
int p = 0;
int t = 0;
if (std::sscanf(&m_currentLine[pos], "%d/%d/%d%n", &p, &t, &n, &offset) != 3)
{
if (std::sscanf(&m_currentLine[pos], "%d//%d%n", &p, &n, &offset) != 2)
{
if (std::sscanf(&m_currentLine[pos], "%d/%d%n", &p, &t, &offset) != 2)
{
if (std::sscanf(&m_currentLine[pos], "%d%n", &p, &offset) != 1)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
error = true;
break;
}
}
}
}
if (p < 0)
{
p += static_cast<int>(m_positions.size());
if (p < 0)
{
Error("Vertex index out of range (" + std::to_string(p) + " < 0");
error = true;
break;
}
++p;
}
if (n < 0)
{
n += static_cast<int>(m_normals.size());
if (n < 0)
{
Error("Normal index out of range (" + std::to_string(n) + " < 0");
error = true;
break;
}
++n;
}
if (t < 0)
{
t += static_cast<int>(m_texCoords.size());
if (t < 0)
{
Error("Texture coordinates index out of range (" + std::to_string(t) + " < 0");
error = true;
break;
}
++t;
}
if (static_cast<std::size_t>(p) > m_positions.size())
{
Error("Vertex index out of range (" + std::to_string(p) + " >= " + std::to_string(m_positions.size()) + ')');
error = true;
break;
}
else if (n != 0 && static_cast<std::size_t>(n) > m_normals.size())
{
Error("Normal index out of range (" + std::to_string(n) + " >= " + std::to_string(m_normals.size()) + ')');
error = true;
break;
}
else if (t != 0 && static_cast<std::size_t>(t) > m_texCoords.size())
{
Error("TexCoord index out of range (" + std::to_string(t) + " >= " + std::to_string(m_texCoords.size()) + ')');
error = true;
break;
}
currentMesh->vertices[face.firstVertex + i].normal = static_cast<UInt32>(n);
currentMesh->vertices[face.firstVertex + i].position = static_cast<UInt32>(p);
currentMesh->vertices[face.firstVertex + i].texCoord = static_cast<UInt32>(t);
pos += offset;
}
if (!error)
currentMesh->faces.push_back(std::move(face));
else
currentMesh->vertices.resize(face.firstVertex); //< Remove vertices
break;
}
case 'm': //< MTLLib
{
const char prefix[] = "mtllib ";
if (!StartsWith(m_currentLine, prefix))
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
m_mtlLib = m_currentLine.substr(sizeof(prefix) - 1);
break;
}
case 'g': //< Group (inside a mesh)
case 'o': //< Object (defines a mesh)
{
if (m_currentLine.size() <= 2 || m_currentLine[1] != ' ')
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
std::string objectName = m_currentLine.substr(2);
if (objectName.empty())
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
meshName = objectName;
currentMesh = nullptr;
break;
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
case 's': //< Smooth
if (m_currentLine.size() <= 2 || m_currentLine[1] == ' ')
{
std::string param = m_currentLine.substr(2);
if (param != "all" && param != "on" && param != "off" && !IsNumber(param))
{
if (!UnrecognizedLine())
return false;
}
}
else if (!UnrecognizedLine())
return false;
break;
#endif
case 'u': //< Usemtl
{
const char prefix[] = "usemtl ";
if (!StartsWith(m_currentLine, prefix))
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
std::string newMatName = m_currentLine.substr(sizeof(prefix) - 1);
if (newMatName.empty())
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
matName = std::move(newMatName);
currentMesh = nullptr;
break;
}
case 'v': //< Position/Normal/Texcoords
{
if (m_currentLine.size() < 7)
{
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
if (std::isspace(m_currentLine[1]))
{
Vector4f vertex(Vector3f::Zero(), 1.f);
unsigned int paramCount = std::sscanf(&m_currentLine[2], " %f %f %f %f", &vertex.x, &vertex.y, &vertex.z, &vertex.w);
if (paramCount >= 1)
m_positions.push_back(vertex);
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else if (!UnrecognizedLine())
return false;
#endif
}
else if (m_currentLine[1] == 'n' && std::isspace(m_currentLine[2]))
{
Vector3f normal(Vector3f::Zero());
unsigned int paramCount = std::sscanf(&m_currentLine[3], " %f %f %f", &normal.x, &normal.y, &normal.z);
if (paramCount == 3)
m_normals.push_back(normal);
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else if (!UnrecognizedLine())
return false;
#endif
}
else if (m_currentLine[1] == 't' && std::isspace(m_currentLine[2]))
{
Vector3f uvw(Vector3f::Zero());
unsigned int paramCount = std::sscanf(&m_currentLine[3], " %f %f %f", &uvw.x, &uvw.y, &uvw.z);
if (paramCount >= 2)
m_texCoords.push_back(uvw);
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else if (!UnrecognizedLine())
return false;
#endif
}
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
else if (!UnrecognizedLine())
return false;
#endif
break;
}
default:
#if NAZARA_CORE_STRICT_RESOURCE_PARSING
if (!UnrecognizedLine())
return false;
#endif
break;
}
}
std::unordered_map<std::string, unsigned int> materials;
m_materials.resize(matCount);
for (auto meshIt = meshesByName.begin(); meshIt != meshesByName.end(); ++meshIt)
{
auto& matMap = meshIt.value();
for (auto matIt = matMap.begin(); matIt != matMap.end(); ++matIt)
{
MatPair& matPair = matIt.value();
Mesh& mesh = matPair.first;
unsigned int index = matPair.second;
if (!mesh.faces.empty())
{
const std::string& matKey = matIt.key();
mesh.name = meshIt.key();
auto it = materials.find(matKey);
if (it == materials.end())
{
mesh.material = index;
materials[matKey] = index;
m_materials[index] = matKey;
}
else
mesh.material = it->second;
m_meshes.emplace_back(std::move(mesh));
}
}
}
if (m_meshes.empty())
{
NazaraError("no meshes");
return false;
}
return true;
}
bool OBJParser::Save(Stream& stream) const
{
m_currentStream = &stream;
// force stream in text mode, reset it at the end
CallOnExit resetTextMode([&stream]
{
stream.EnableTextMode(false);
});
if ((stream.GetStreamOptions() & StreamOption::Text) == 0)
stream.EnableTextMode(true);
else
resetTextMode.Reset();
m_outputStream.str({});
EmitLine("# Exported by Nazara Engine");
EmitLine();
if (!m_mtlLib.empty())
{
Emit("mtllib ");
EmitLine(PathToString(m_mtlLib));
EmitLine();
}
Emit("# position count: ");
EmitLine(m_positions.size());
for (const Vector4f& position : m_positions)
{
Emit("v ");
Emit(position.x);
Emit(' ');
Emit(position.y);
Emit(' ');
Emit(position.z);
if (!NumberEquals(position.w, 1.f))
{
Emit(' ');
Emit(position.w);
}
EmitLine();
}
EmitLine();
if (!m_normals.empty())
{
Emit("# normal count: ");
EmitLine(m_normals.size());
for (const Nz::Vector3f& normal : m_normals)
{
Emit("vn ");
Emit(normal.x);
Emit(' ');
Emit(normal.y);
Emit(' ');
Emit(normal.y);
EmitLine();
}
EmitLine();
}
if (!m_texCoords.empty())
{
Emit("# texcoords count: ");
EmitLine(m_texCoords.size());
for (const Nz::Vector3f& uvw : m_texCoords)
{
Emit("vt ");
Emit(uvw.x);
Emit(' ');
Emit(uvw.y);
if (NumberEquals(uvw.z, 0.f))
{
Emit(' ');
Emit(uvw.z);
}
EmitLine();
}
EmitLine();
}
std::unordered_map<std::size_t /* mesh */, std::vector<std::size_t> /* meshes*/> meshesByMaterials;
std::size_t meshIndex = 0;
for (const Mesh& mesh : m_meshes)
meshesByMaterials[mesh.material].push_back(meshIndex++);
for (auto& pair : meshesByMaterials)
{
Emit("usemtl ");
EmitLine(m_materials[pair.first]);
Emit("# groups count: ");
EmitLine(pair.second.size());
EmitLine();
for (std::size_t index : pair.second)
{
const Mesh& mesh = m_meshes[index];
Emit("g ");
EmitLine(mesh.name);
EmitLine();
Emit("# face count: ");
EmitLine(mesh.faces.size());
Emit("# vertex count: ");
EmitLine(mesh.vertices.size());
for (const Face& face : mesh.faces)
{
Emit('f');
for (std::size_t i = 0; i < face.vertexCount; ++i)
{
Emit(' ');
const FaceVertex& faceVertex = mesh.vertices[face.firstVertex + i];
Emit(faceVertex.position);
if (faceVertex.texCoord != 0 || faceVertex.normal != 0)
{
Emit('/');
if (faceVertex.texCoord != 0)
Emit(faceVertex.texCoord);
if (faceVertex.normal != 0)
{
Emit('/');
Emit(faceVertex.normal);
}
}
}
EmitLine();
}
}
EmitLine();
}
Flush();
return true;
}
bool OBJParser::Advance(bool required)
{
if (!m_keepLastLine)
{
do
{
if (m_currentStream->EndOfStream())
{
if (required)
Error("Incomplete OBJ file");
return false;
}
m_lineCount++;
m_currentLine = m_currentStream->ReadLine();
if (std::size_t p = m_currentLine.find('#'); p != m_currentLine.npos)
{
if (p > 0)
m_currentLine = m_currentLine.substr(0, p - 1);
else
m_currentLine.clear();
}
m_currentLine = Trim(m_currentLine);
if (m_currentLine.empty())
continue;
}
while (m_currentLine.empty());
}
else
m_keepLastLine = false;
return true;
}
void OBJParser::Flush() const
{
m_currentStream->Write(std::move(m_outputStream).str());
m_outputStream.str({});
}
}

View File

@@ -0,0 +1,233 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/OBJSaver.hpp>
#include <Nazara/Core/MaterialData.hpp>
#include <Nazara/Core/Mesh.hpp>
#include <Nazara/Core/StaticMesh.hpp>
#include <Nazara/Core/TriangleIterator.hpp>
#include <Nazara/Core/VertexMapper.hpp>
#include <Nazara/Core/Formats/MTLParser.hpp>
#include <Nazara/Core/Formats/OBJParser.hpp>
#include <map>
#include <unordered_set>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace NAZARA_ANONYMOUS_NAMESPACE
{
template<typename T>
class VertexCache
{
public:
VertexCache(T* ptr) :
m_count(0),
m_buffer(ptr)
{
}
std::size_t GetCount() const
{
return m_count;
}
std::size_t Insert(const T& data)
{
auto it = m_cache.find(data);
if (it == m_cache.end())
{
it = m_cache.insert(std::make_pair(data, m_count)).first;
m_buffer[m_count] = data;
m_count++;
}
return it->second + 1;
}
private:
std::size_t m_count;
std::map<T, std::size_t> m_cache;
T* m_buffer;
};
bool IsOBJSupportedSave(std::string_view extension)
{
return (extension == ".obj");
}
bool SaveOBJToStream(const Mesh& mesh, std::string_view format, Stream& stream, const MeshParams& parameters)
{
NAZARA_USE_ANONYMOUS_NAMESPACE
NazaraUnused(parameters);
if (!mesh.IsValid())
{
NazaraError("invalid mesh");
return false;
}
if (mesh.IsAnimable())
{
NazaraErrorFmt("an animated mesh cannot be saved to {0} format", format);
return false;
}
std::size_t worstCacheVertexCount = mesh.GetVertexCount();
OBJParser objFormat;
objFormat.SetNormalCount(worstCacheVertexCount);
objFormat.SetPositionCount(worstCacheVertexCount);
objFormat.SetTexCoordCount(worstCacheVertexCount);
std::filesystem::path mtlPath = stream.GetPath();
if (!mtlPath.empty())
{
mtlPath.replace_extension(".mtl");
std::filesystem::path fileName = mtlPath.filename();
if (!fileName.empty())
objFormat.SetMtlLib(fileName);
}
VertexCache<Vector3f> normalCache(objFormat.GetNormals());
VertexCache<Vector4f> positionCache(objFormat.GetPositions());
VertexCache<Vector3f> texCoordsCache(objFormat.GetTexCoords());
// Materials
MTLParser mtlFormat;
std::unordered_set<std::string> registredMaterials;
std::size_t matCount = mesh.GetMaterialCount();
std::string* materialNames = objFormat.SetMaterialCount(matCount);
for (std::size_t i = 0; i < matCount; ++i)
{
const ParameterList& matData = mesh.GetMaterialData(i);
std::string name;
if (auto result = matData.GetStringParameter(MaterialData::Name))
name = std::move(result).GetValue();
else
name = "material_" + std::to_string(i);
// Makes sure we only have one material of that name
while (registredMaterials.find(name) != registredMaterials.end())
name += '_';
registredMaterials.insert(name);
materialNames[i] = name;
MTLParser::Material* material = mtlFormat.AddMaterial(name);
auto pathResult = matData.GetStringParameter(MaterialData::FilePath);
if (!pathResult)
{
if (auto result = matData.GetColorParameter(MaterialData::AmbientColor))
material->ambient = result.GetValue();
if (auto result = matData.GetColorParameter(MaterialData::BaseColor))
material->diffuse = result.GetValue();
if (auto result = matData.GetColorParameter(MaterialData::SpecularColor))
material->specular = result.GetValue();
if (auto result = matData.GetDoubleParameter(MaterialData::Shininess))
material->shininess = SafeCast<float>(result.GetValue());
if (auto result = matData.GetStringParameter(MaterialData::AlphaTexturePath))
material->alphaMap = std::move(result).GetValue();
if (auto result = matData.GetStringParameter(MaterialData::BaseColorTexturePath))
material->diffuseMap = std::move(result).GetValue();
if (auto result = matData.GetStringParameter(MaterialData::SpecularTexturePath))
material->specularMap = std::move(result).GetValue();
}
else
material->diffuseMap = std::move(pathResult).GetValue();
}
// Meshes
std::size_t meshCount = mesh.GetSubMeshCount();
OBJParser::Mesh* meshes = objFormat.SetMeshCount(meshCount);
for (std::size_t i = 0; i < meshCount; ++i)
{
StaticMesh& staticMesh = static_cast<StaticMesh&>(*mesh.GetSubMesh(i));
std::size_t triangleCount = staticMesh.GetTriangleCount();
meshes[i].faces.resize(triangleCount);
meshes[i].material = staticMesh.GetMaterialIndex();
meshes[i].name = "mesh_" + std::to_string(i);
meshes[i].vertices.resize(triangleCount * 3);
{
VertexMapper vertexMapper(staticMesh);
SparsePtr<Vector3f> normalPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Normal);
SparsePtr<Vector3f> positionPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Position);
SparsePtr<Vector2f> texCoordsPtr = vertexMapper.GetComponentPtr<Vector2f>(VertexComponent::TexCoord);
std::size_t faceIndex = 0;
TriangleIterator triangleIt(staticMesh);
do
{
OBJParser::Face& face = meshes[i].faces[faceIndex];
face.firstVertex = faceIndex * 3;
face.vertexCount = 3;
for (unsigned int j = 0; j < 3; ++j)
{
OBJParser::FaceVertex& vertexIndices = meshes[i].vertices[face.firstVertex + j];
std::size_t index = triangleIt[j];
vertexIndices.position = positionCache.Insert(positionPtr[index]);
if (normalPtr)
vertexIndices.normal = normalCache.Insert(normalPtr[index]);
else
vertexIndices.normal = 0;
if (texCoordsPtr)
vertexIndices.texCoord = texCoordsCache.Insert(texCoordsPtr[index]);
else
vertexIndices.texCoord = 0;
}
faceIndex++;
}
while (triangleIt.Advance());
}
}
objFormat.SetNormalCount(normalCache.GetCount());
objFormat.SetPositionCount(positionCache.GetCount());
objFormat.SetTexCoordCount(texCoordsCache.GetCount());
objFormat.Save(stream);
if (!mtlPath.empty())
{
File mtlFile(mtlPath, OpenMode::Write | OpenMode::Truncate);
if (mtlFile.IsOpen())
mtlFormat.Save(mtlFile);
}
return true;
}
}
namespace Loaders
{
MeshSaver::Entry GetMeshSaver_OBJ()
{
NAZARA_USE_ANONYMOUS_NAMESPACE
MeshSaver::Entry entry;
entry.formatSupport = IsOBJSupportedSave;
entry.streamSaver = SaveOBJToStream;
return entry;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_OBJSAVER_HPP
#define NAZARA_CORE_FORMATS_OBJSAVER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Mesh.hpp>
namespace Nz::Loaders
{
MeshSaver::Entry GetMeshSaver_OBJ();
}
#endif // NAZARA_CORE_FORMATS_OBJSAVER_HPP

View File

@@ -0,0 +1,343 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/PCXLoader.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Image.hpp>
#include <Nazara/Core/Stream.hpp>
#include <NazaraUtils/Endianness.hpp>
#include <memory>
#include <Nazara/Core/Debug.hpp>
// Auteur du loader original : David Henry
namespace Nz
{
namespace
{
struct PCXHeader
{
UInt8 manufacturer;
UInt8 version;
UInt8 encoding;
UInt8 bitsPerPixel;
UInt16 xmin, ymin;
UInt16 xmax, ymax;
UInt16 horzRes, vertRes;
UInt8 palette[48];
UInt8 reserved;
UInt8 numColorPlanes;
UInt16 bytesPerScanLine;
UInt16 paletteType;
UInt16 horzSize, vertSize;
UInt8 padding[54];
};
static_assert(sizeof(PCXHeader) == (6+48+54)*sizeof(UInt8) + 10*sizeof(UInt16), "pcx_header struct must be packed");
bool IsPCXSupported(std::string_view extension)
{
return (extension == ".pcx");
}
Result<std::shared_ptr<Image>, ResourceLoadingError> LoadPCX(Stream& stream, const ImageParams& parameters)
{
PCXHeader header;
if (stream.Read(&header, sizeof(PCXHeader)) != sizeof(PCXHeader))
return Err(ResourceLoadingError::Unrecognized);
if (header.manufacturer != 0x0a)
return Err(ResourceLoadingError::Unrecognized);
#ifdef NAZARA_BIG_ENDIAN
// PCX files are little-endian
header.xmin = ByteSwap(header.xmin);
header.ymin = ByteSwap(header.ymin);
header.xmax = ByteSwap(header.xmax);
header.ymax = ByteSwap(header.ymax);
header.horzRes = ByteSwap(header.horzRes);
header.vertRes = ByteSwap(header.vertRes);
header.bytesPerScanLine = ByteSwap(header.bytesPerScanLine);
header.paletteType = ByteSwap(header.paletteType);
header.horzSize = ByteSwap(header.horzSize);
header.vertSize = ByteSwap(header.vertSize);
#endif
unsigned int bitCount = header.bitsPerPixel * header.numColorPlanes;
unsigned int width = header.xmax - header.xmin+1;
unsigned int height = header.ymax - header.ymin+1;
std::shared_ptr<Image> image = std::make_shared<Image>();
if (!image->Create(ImageType::E2D, PixelFormat::RGB8, width, height, 1, (parameters.levelCount > 0) ? parameters.levelCount : 1))
{
NazaraError("failed to create image");
return Err(ResourceLoadingError::Internal);
}
UInt8* pixels = image->GetPixels();
UInt8 rleValue = 0;
UInt8 rleCount = 0;
switch (bitCount)
{
case 1:
{
for (unsigned int y = 0; y < height; ++y)
{
UInt8* ptr = &pixels[y * width * 3];
int bytes = header.bytesPerScanLine;
/* decode line number y */
while (bytes--)
{
if (rleCount == 0)
{
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
if (rleValue < 0xc0)
rleCount = 1;
else
{
rleCount = rleValue - 0xc0;
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
}
}
rleCount--;
for (int i = 7; i >= 0; --i)
{
int colorIndex = ((rleValue & (1 << i)) > 0);
*ptr++ = header.palette[colorIndex * 3 + 0];
*ptr++ = header.palette[colorIndex * 3 + 1];
*ptr++ = header.palette[colorIndex * 3 + 2];
}
}
}
break;
}
case 4:
{
std::unique_ptr<UInt8[]> colorIndex(new UInt8[width]);
std::unique_ptr<UInt8[]> line(new UInt8[header.bytesPerScanLine]);
for (unsigned int y = 0; y < height; ++y)
{
UInt8* ptr = &pixels[y * width * 3];
std::memset(colorIndex.get(), 0, width);
for (unsigned int c = 0; c < 4; ++c)
{
UInt8* pLine = line.get();
int bytes = header.bytesPerScanLine;
/* decode line number y */
while (bytes--)
{
if (rleCount == 0)
{
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
if (rleValue < 0xc0)
rleCount = 1;
else
{
rleCount = rleValue - 0xc0;
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
}
}
rleCount--;
*(pLine++) = rleValue;
}
/* compute line's color indexes */
for (unsigned int x = 0; x < width; ++x)
{
if (line[x / 8] & (128 >> (x % 8)))
colorIndex[x] += (1 << c);
}
}
/* decode scan line. color index => rgb */
for (unsigned int x = 0; x < width; ++x)
{
*ptr++ = header.palette[colorIndex[x] * 3 + 0];
*ptr++ = header.palette[colorIndex[x] * 3 + 1];
*ptr++ = header.palette[colorIndex[x] * 3 + 2];
}
}
break;
}
case 8:
{
UInt8 palette[768];
/* the palette is contained in the last 769 bytes of the file */
UInt64 curPos = stream.GetCursorPos();
stream.SetCursorPos(stream.GetSize()-769);
UInt8 magic;
if (!stream.Read(&magic, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
/* first byte must be equal to 0x0c (12) */
if (magic != 0x0c)
{
NazaraErrorFmt("Colormap's first byte must be 0x0c ({0:#x})", magic);
return Err(ResourceLoadingError::DecodingError);
}
/* read palette */
if (stream.Read(palette, 768) != 768)
{
NazaraError("failed to read palette");
return Err(ResourceLoadingError::DecodingError);
}
stream.SetCursorPos(curPos);
/* read pixel data */
for (unsigned int y = 0; y < height; ++y)
{
UInt8* ptr = &pixels[y * width * 3];
int bytes = header.bytesPerScanLine;
/* decode line number y */
while (bytes--)
{
if (rleCount == 0)
{
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
if (rleValue < 0xc0)
rleCount = 1;
else
{
rleCount = rleValue - 0xc0;
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
}
}
rleCount--;
*ptr++ = palette[rleValue * 3 + 0];
*ptr++ = palette[rleValue * 3 + 1];
*ptr++ = palette[rleValue * 3 + 2];
}
}
break;
}
case 24:
{
for (unsigned int y = 0; y < height; ++y)
{
/* for each color plane */
for (int c = 0; c < 3; ++c)
{
UInt8* ptr = &pixels[y * width * 3];
int bytes = header.bytesPerScanLine;
/* decode line number y */
while (bytes--)
{
if (rleCount == 0)
{
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
if (rleValue < 0xc0)
rleCount = 1;
else
{
rleCount = rleValue - 0xc0;
if (!stream.Read(&rleValue, 1))
{
NazaraErrorFmt("failed to read stream (byte {0})", stream.GetCursorPos());
return Err(ResourceLoadingError::DecodingError);
}
}
}
rleCount--;
ptr[c] = static_cast<UInt8>(rleValue);
ptr += 3;
}
}
}
break;
}
default:
NazaraErrorFmt("unsupported {0} bitcount for pcx files", bitCount);
return Err(ResourceLoadingError::DecodingError);
}
if (parameters.loadFormat != PixelFormat::Undefined)
image->Convert(parameters.loadFormat);
return image;
}
}
namespace Loaders
{
ImageLoader::Entry GetImageLoader_PCX()
{
ImageLoader::Entry loaderEntry;
loaderEntry.extensionSupport = IsPCXSupported;
loaderEntry.streamLoader = LoadPCX;
loaderEntry.parameterFilter = [](const ImageParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinPCXLoader"); result.GetValueOr(false))
return false;
return true;
};
return loaderEntry;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_PCXLOADER_HPP
#define NAZARA_CORE_FORMATS_PCXLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Image.hpp>
namespace Nz::Loaders
{
ImageLoader::Entry GetImageLoader_PCX();
}
#endif // NAZARA_CORE_FORMATS_PCXLOADER_HPP

View File

@@ -0,0 +1,120 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/STBLoader.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Image.hpp>
#include <Nazara/Core/Stream.hpp>
#include <NazaraUtils/CallOnExit.hpp>
#include <NazaraUtils/Endianness.hpp>
#include <frozen/string.h>
#include <frozen/unordered_set.h>
#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace NAZARA_ANONYMOUS_NAMESPACE
{
int StbiEof(void* userdata)
{
Stream* stream = static_cast<Stream*>(userdata);
return stream->GetCursorPos() >= stream->GetSize();
}
int StbiRead(void* userdata, char* data, int size)
{
Stream* stream = static_cast<Stream*>(userdata);
return static_cast<int>(stream->Read(data, size));
}
void StbiSkip(void* userdata, int size)
{
Stream* stream = static_cast<Stream*>(userdata);
stream->SetCursorPos(static_cast<Int64>(stream->GetCursorPos()) + static_cast<Int64>(size));
}
static stbi_io_callbacks s_stbiCallbacks = { StbiRead, StbiSkip, StbiEof };
bool IsSTBSupported(std::string_view extension)
{
constexpr auto s_supportedExtensions = frozen::make_unordered_set<frozen::string>({ ".bmp", ".gif", ".hdr", ".jpg", ".jpeg", ".pic", ".png", ".ppm", ".pgm", ".psd", ".tga" });
return s_supportedExtensions.find(extension) != s_supportedExtensions.end();
}
Result<std::shared_ptr<Image>, ResourceLoadingError> LoadSTB(Stream& stream, const ImageParams& parameters)
{
UInt64 streamPos = stream.GetCursorPos();
int width, height, bpp;
if (!stbi_info_from_callbacks(&s_stbiCallbacks, &stream, &width, &height, &bpp))
return Err(ResourceLoadingError::Unrecognized);
stream.SetCursorPos(streamPos);
// Load everything as RGBA8 and then convert using the Image::Convert method
// This is because of a STB bug when loading some JPG images with default settings
UInt8* ptr = stbi_load_from_callbacks(&s_stbiCallbacks, &stream, &width, &height, &bpp, STBI_rgb_alpha);
if (!ptr)
{
NazaraErrorFmt("failed to load image: {0}", std::string(stbi_failure_reason()));
return Err(ResourceLoadingError::DecodingError);
}
CallOnExit freeStbiImage([ptr]()
{
stbi_image_free(ptr);
});
std::shared_ptr<Image> image = std::make_shared<Image>();
if (!image->Create(ImageType::E2D, PixelFormat::RGBA8, width, height, 1, (parameters.levelCount > 0) ? parameters.levelCount : 1))
{
NazaraError("failed to create image");
return Err(ResourceLoadingError::Internal);
}
image->Update(ptr);
freeStbiImage.CallAndReset();
if (parameters.loadFormat != PixelFormat::Undefined)
{
if (!image->Convert(parameters.loadFormat))
{
NazaraError("failed to convert image to required format");
return Err(ResourceLoadingError::Internal);
}
}
return image;
}
}
namespace Loaders
{
ImageLoader::Entry GetImageLoader_STB()
{
NAZARA_USE_ANONYMOUS_NAMESPACE
ImageLoader::Entry loaderEntry;
loaderEntry.extensionSupport = IsSTBSupported;
loaderEntry.streamLoader = LoadSTB;
loaderEntry.parameterFilter = [](const ImageParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipBuiltinSTBLoader"); result.GetValueOr(false))
return false;
return true;
};
return loaderEntry;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_STBLOADER_HPP
#define NAZARA_CORE_FORMATS_STBLOADER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Image.hpp>
namespace Nz::Loaders
{
ImageLoader::Entry GetImageLoader_STB();
}
#endif // NAZARA_CORE_FORMATS_STBLOADER_HPP

View File

@@ -0,0 +1,284 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Core/Formats/STBSaver.hpp>
#include <Nazara/Core/Image.hpp>
#include <Nazara/Core/PixelFormat.hpp>
#include <Nazara/Core/Formats/STBLoader.hpp>
#include <frozen/string.h>
#include <frozen/unordered_map.h>
#include <stdexcept>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
namespace NAZARA_ANONYMOUS_NAMESPACE
{
using FormatHandler = bool(*)(const Image& image, const ImageParams& parameters, Stream& stream);
int ConvertToFloatFormat(Image& image)
{
switch (image.GetFormat())
{
case PixelFormat::R32F:
return 1;
case PixelFormat::RG32F:
return 2;
case PixelFormat::RGB32F:
return 3;
case PixelFormat::RGBA32F:
return 4;
default:
{
if (PixelFormatInfo::HasAlpha(image.GetFormat()))
{
if (!image.Convert(PixelFormat::RGBA32F))
break;
return 4;
}
else
{
if (!image.Convert(PixelFormat::RGB32F))
break;
return 3;
}
}
}
return 0;
}
int ConvertToIntegerFormat(Image& image)
{
switch (image.GetFormat())
{
case PixelFormat::L8:
case PixelFormat::R8:
return 1;
case PixelFormat::LA8:
case PixelFormat::RG8:
return 2;
case PixelFormat::RGB8:
return 3;
case PixelFormat::RGBA8:
return 4;
default:
{
if (PixelFormatInfo::HasAlpha(image.GetFormat()))
{
if (!image.Convert(PixelFormat::RGBA8))
break;
return 4;
}
else
{
if (!image.Convert(PixelFormat::RGB8))
break;
return 3;
}
}
}
return 0;
}
void WriteToStream(void* userdata, void* data, int size)
{
Stream* stream = static_cast<Stream*>(userdata);
if (stream->Write(data, size) != static_cast<std::size_t>(size))
throw std::runtime_error("failed to write to stream");
}
bool SaveBMP(const Image& image, const ImageParams& parameters, Stream& stream)
{
NazaraUnused(parameters);
Image tempImage(image); //< We're using COW here to prevent Image copy unless required
int componentCount = ConvertToIntegerFormat(tempImage);
if (componentCount == 0)
{
NazaraError("failed to convert image to suitable format");
return false;
}
if (!stbi_write_bmp_to_func(&WriteToStream, &stream, tempImage.GetWidth(), tempImage.GetHeight(), componentCount, tempImage.GetConstPixels()))
{
NazaraError("failed to write BMP to stream");
return false;
}
return true;
}
bool SaveJPEG(const Image& image, const ImageParams& parameters, Stream& stream)
{
Image tempImage(image); //< We're using COW here to prevent Image copy unless required
int componentCount = ConvertToIntegerFormat(tempImage);
if (componentCount == 0)
{
NazaraError("failed to convert image to suitable format");
return false;
}
long long imageQuality = parameters.custom.GetIntegerParameter("JPEGQuality").GetValueOr(100);
if (imageQuality <= 0 || imageQuality > 100)
{
NazaraErrorFmt("NativeJPEGSaver_Quality value ({0}) does not fit in bounds ]0, 100], clamping...", imageQuality);
imageQuality = Nz::Clamp(imageQuality, 1LL, 100LL);
}
if (!stbi_write_jpg_to_func(&WriteToStream, &stream, tempImage.GetWidth(), tempImage.GetHeight(), componentCount, tempImage.GetConstPixels(), int(imageQuality)))
{
NazaraError("failed to write JPEG to stream");
return false;
}
return true;
}
bool SaveHDR(const Image& image, const ImageParams& parameters, Stream& stream)
{
NazaraUnused(parameters);
Image tempImage(image); //< We're using COW here to prevent Image copy unless required
int componentCount = ConvertToFloatFormat(tempImage);
if (componentCount == 0)
{
NazaraError("failed to convert image to suitable format");
return false;
}
if (!stbi_write_hdr_to_func(&WriteToStream, &stream, tempImage.GetWidth(), tempImage.GetHeight(), componentCount, reinterpret_cast<const float*>(tempImage.GetConstPixels())))
{
NazaraError("failed to write HDR to stream");
return false;
}
return true;
}
bool SavePNG(const Image& image, const ImageParams& parameters, Stream& stream)
{
NazaraUnused(parameters);
Image tempImage(image); //< We're using COW here to prevent Image copy unless required
int componentCount = ConvertToIntegerFormat(tempImage);
if (componentCount == 0)
{
NazaraError("failed to convert image to suitable format");
return false;
}
if (!stbi_write_png_to_func(&WriteToStream, &stream, tempImage.GetWidth(), tempImage.GetHeight(), componentCount, tempImage.GetConstPixels(), 0))
{
NazaraError("failed to write PNG to stream");
return false;
}
return true;
}
bool SaveTGA(const Image& image, const ImageParams& parameters, Stream& stream)
{
NazaraUnused(parameters);
Image tempImage(image); //< We're using COW here to prevent Image copy unless required
int componentCount = ConvertToIntegerFormat(tempImage);
if (componentCount == 0)
{
NazaraError("failed to convert image to suitable format");
return false;
}
if (!stbi_write_tga_to_func(&WriteToStream, &stream, tempImage.GetWidth(), tempImage.GetHeight(), componentCount, tempImage.GetConstPixels()))
{
NazaraError("failed to write TGA to stream");
return false;
}
return true;
}
constexpr FormatHandler test = &SaveBMP;
constexpr frozen::unordered_map s_formatHandlers = frozen::make_unordered_map<frozen::string, FormatHandler>({
{ ".bmp", &SaveBMP },
{ ".hdr", &SaveHDR },
{ ".jpg", &SaveJPEG },
{ ".jpeg", &SaveJPEG },
{ ".png", &SavePNG },
{ ".tga", &SaveTGA }
});
bool FormatQuerier(std::string_view extension)
{
return s_formatHandlers.find(extension) != s_formatHandlers.end();
}
bool SaveToStream(const Image& image, std::string_view format, Stream& stream, const ImageParams& parameters)
{
NazaraUnused(parameters);
if (!image.IsValid())
{
NazaraError("invalid image");
return false;
}
ImageType type = image.GetType();
if (type != ImageType::E1D && type != ImageType::E2D)
{
NazaraErrorFmt("Image type {0:#x}) is not in a supported format", UnderlyingCast(type));
return false;
}
auto it = s_formatHandlers.find(format);
NazaraAssert(it != s_formatHandlers.end(), "Invalid handler");
const FormatHandler& handler = it->second;
try
{
return handler(image, parameters, stream);
}
catch (const std::exception& e)
{
NazaraError(e.what());
return false;
}
}
}
namespace Loaders
{
ImageSaver::Entry GetImageSaver_STB()
{
NAZARA_USE_ANONYMOUS_NAMESPACE
ImageSaver::Entry entry;
entry.formatSupport = FormatQuerier;
entry.streamSaver = SaveToStream;
return entry;
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#pragma once
#ifndef NAZARA_CORE_FORMATS_STBSAVER_HPP
#define NAZARA_CORE_FORMATS_STBSAVER_HPP
#include <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Image.hpp>
namespace Nz::Loaders
{
ImageSaver::Entry GetImageSaver_STB();
}
#endif // NAZARA_CORE_FORMATS_STBSAVER_HPP