Remove Utility module and move its content to Core and TextRenderer modules
This commit is contained in:
committed by
Jérôme Leclercq
parent
965a00182c
commit
e64c2b036e
91
src/Nazara/Core/Formats/DDSConstants.cpp
Normal file
91
src/Nazara/Core/Formats/DDSConstants.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
377
src/Nazara/Core/Formats/DDSConstants.hpp
Normal file
377
src/Nazara/Core/Formats/DDSConstants.hpp
Normal 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
|
||||
270
src/Nazara/Core/Formats/DDSLoader.cpp
Normal file
270
src/Nazara/Core/Formats/DDSLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/DDSLoader.hpp
Normal file
18
src/Nazara/Core/Formats/DDSLoader.hpp
Normal 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
|
||||
761
src/Nazara/Core/Formats/GIFLoader.cpp
Normal file
761
src/Nazara/Core/Formats/GIFLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Nazara/Core/Formats/GIFLoader.hpp
Normal file
19
src/Nazara/Core/Formats/GIFLoader.hpp
Normal 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
|
||||
177
src/Nazara/Core/Formats/MD2Constants.cpp
Normal file
177
src/Nazara/Core/Formats/MD2Constants.cpp
Normal 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)
|
||||
};
|
||||
}
|
||||
69
src/Nazara/Core/Formats/MD2Constants.hpp
Normal file
69
src/Nazara/Core/Formats/MD2Constants.hpp
Normal 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
|
||||
271
src/Nazara/Core/Formats/MD2Loader.cpp
Normal file
271
src/Nazara/Core/Formats/MD2Loader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/MD2Loader.hpp
Normal file
18
src/Nazara/Core/Formats/MD2Loader.hpp
Normal 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
|
||||
112
src/Nazara/Core/Formats/MD5AnimLoader.cpp
Normal file
112
src/Nazara/Core/Formats/MD5AnimLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/MD5AnimLoader.hpp
Normal file
18
src/Nazara/Core/Formats/MD5AnimLoader.hpp
Normal 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
|
||||
522
src/Nazara/Core/Formats/MD5AnimParser.cpp
Normal file
522
src/Nazara/Core/Formats/MD5AnimParser.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
373
src/Nazara/Core/Formats/MD5MeshLoader.cpp
Normal file
373
src/Nazara/Core/Formats/MD5MeshLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/MD5MeshLoader.hpp
Normal file
18
src/Nazara/Core/Formats/MD5MeshLoader.hpp
Normal 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
|
||||
462
src/Nazara/Core/Formats/MD5MeshParser.cpp
Normal file
462
src/Nazara/Core/Formats/MD5MeshParser.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
660
src/Nazara/Core/Formats/MTLParser.cpp
Normal file
660
src/Nazara/Core/Formats/MTLParser.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
371
src/Nazara/Core/Formats/OBJLoader.cpp
Normal file
371
src/Nazara/Core/Formats/OBJLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/OBJLoader.hpp
Normal file
18
src/Nazara/Core/Formats/OBJLoader.hpp
Normal 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
|
||||
679
src/Nazara/Core/Formats/OBJParser.cpp
Normal file
679
src/Nazara/Core/Formats/OBJParser.cpp
Normal 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({});
|
||||
}
|
||||
}
|
||||
233
src/Nazara/Core/Formats/OBJSaver.cpp
Normal file
233
src/Nazara/Core/Formats/OBJSaver.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/OBJSaver.hpp
Normal file
18
src/Nazara/Core/Formats/OBJSaver.hpp
Normal 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
|
||||
343
src/Nazara/Core/Formats/PCXLoader.cpp
Normal file
343
src/Nazara/Core/Formats/PCXLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/PCXLoader.hpp
Normal file
18
src/Nazara/Core/Formats/PCXLoader.hpp
Normal 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
|
||||
120
src/Nazara/Core/Formats/STBLoader.cpp
Normal file
120
src/Nazara/Core/Formats/STBLoader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/STBLoader.hpp
Normal file
18
src/Nazara/Core/Formats/STBLoader.hpp
Normal 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
|
||||
284
src/Nazara/Core/Formats/STBSaver.cpp
Normal file
284
src/Nazara/Core/Formats/STBSaver.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Nazara/Core/Formats/STBSaver.hpp
Normal file
18
src/Nazara/Core/Formats/STBSaver.hpp
Normal 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
|
||||
Reference in New Issue
Block a user