mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 05:09:20 +02:00
* Text displays: accept characters above 127, in windows-1250 or utf-8 A text display (sub-model of type "text") looks up the sub-model of each character in a 256 entry table, and indexed it with a plain char. With char being signed, any character above 127 gave a negative index: reading the text went outside the table, and so did filling the table if a character sub-model had such a name. In effect the displays could only show ASCII, and a text with national characters could crash. The lookup is now keyed by unicode code point. Both the displayed text and the names of the character sub-models are decoded as utf-8 if they are valid utf-8, and as windows-1250 otherwise, so legacy (ansi) sceneries and models work next to utf-8 ones, and the two can be mixed. A short ansi text can be valid utf-8 by accident (two upper case accented letters in a row will do); when both readings are possible the one which finds more of its characters in the model is used. The rest is unchanged: the first character of the text still isn't displayed, the characters are still drawn in the translucent pass only. A missing text pointer no longer gets dereferenced. * Text displays: prepare the characters once for each text The sequence of character sub-models was cached for the last text only, so displays of several instances drawn in turn decoded their texts again on every draw. It is now kept for each text the display shows; the texts of the instances don't change, so a draw costs a single lookup. Also the comments added in the renderers are now in English. * Text displays: move the text decoding to utilities utf8_to_utf32() and win1250_to_utf32(), with the windows-1250 table, now sit next to win1250_to_utf8(). The existing function maps only the Polish letters and passes other bytes above 127 through, so it can't serve the displays. * Text displays: address the SonarCloud findings std::array instead of the C-style arrays, the utf-8 sequence length worked out in a function of its own instead of nested conditional operators, std::byte for the bit operations, std::ranges::count_if and contains(). The character loop of the renderers moved to Render_Alpha_text(), which takes the nesting back to the limit, and the static pasText is accessed through the class.
2709 lines
86 KiB
C++
2709 lines
86 KiB
C++
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
/*
|
|
MaSzyna EU07 locomotive simulator
|
|
Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others
|
|
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "model/Model3d.h"
|
|
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/Logs.h"
|
|
#include "utilities/utilities.h"
|
|
#include "rendering/renderer.h"
|
|
#include "utilities/Timer.h"
|
|
#include "simulation/simulation.h"
|
|
#include "simulation/simulationtime.h"
|
|
#include "world/mtable.h"
|
|
#include "scene/sn_utils.h"
|
|
|
|
#include <bit>
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
using namespace Mtable;
|
|
|
|
float TSubModel::fSquareDist = 0.f;
|
|
std::uintptr_t TSubModel::iInstance; // numer renderowanego egzemplarza obiektu
|
|
std::uint32_t TSubModel::iVariantSeed = 0; // ziarno losowania wariantów tekstur renderowanego egzemplarza
|
|
texture_handle const *TSubModel::ReplacableSkinId = nullptr;
|
|
int TSubModel::iAlpha = 0x30300030; // maska do testowania flag tekstur wymiennych
|
|
TModel3d *TSubModel::pRoot; // Ra: tymczasowo wskaźnik na model widoczny z submodelu
|
|
std::string *TSubModel::pasText;
|
|
// przykłady dla TSubModel::iAlpha:
|
|
// 0x30300030 - wszystkie bez kanału alfa
|
|
// 0x31310031 - tekstura -1 używana w danym cyklu, pozostałe nie
|
|
// 0x32320032 - tekstura -2 używana w danym cyklu, pozostałe nie
|
|
// 0x34340034 - tekstura -3 używana w danym cyklu, pozostałe nie
|
|
// 0x38380038 - tekstura -4 używana w danym cyklu, pozostałe nie
|
|
// 0x3F3F003F - wszystkie wymienne tekstury używane w danym cyklu
|
|
// Ale w TModel3d okerśla przezroczystość tekstur wymiennych!
|
|
|
|
TSubModel::~TSubModel()
|
|
{
|
|
|
|
if (iFlags & 0x0200)
|
|
{ // wczytany z pliku tekstowego musi sam posprzątać
|
|
SafeDelete(Next);
|
|
SafeDelete(Child);
|
|
delete fMatrix; // własny transform trzeba usunąć (zawsze jeden)
|
|
}
|
|
};
|
|
|
|
void TSubModel::Name_Material(std::string const &Name)
|
|
{ // ustawienie nazwy submodelu, o
|
|
// ile nie jest wczytany z E3D
|
|
if (iFlags & 0x0200)
|
|
{ // tylko jeżeli submodel zosta utworzony przez new
|
|
m_materialname = Name;
|
|
}
|
|
};
|
|
|
|
void TSubModel::Name(std::string const &Name)
|
|
{ // ustawienie nazwy submodelu, o ile
|
|
// nie jest wczytany z E3D
|
|
if (iFlags & 0x0200)
|
|
pName = Name;
|
|
};
|
|
|
|
// sets rgb components of diffuse color override to specified value
|
|
void TSubModel::SetDiffuseOverride(glm::vec3 const &Color, bool const Includechildren, bool const Includesiblings)
|
|
{
|
|
|
|
if (eType == TP_FREESPOTLIGHT)
|
|
{
|
|
DiffuseOverride = Color;
|
|
}
|
|
if (true == Includesiblings)
|
|
{
|
|
auto sibling{this};
|
|
while ((sibling = sibling->Next) != nullptr)
|
|
{
|
|
sibling->SetDiffuseOverride(Color, Includechildren, false); // no need for all siblings to duplicate the work
|
|
}
|
|
}
|
|
if (true == Includechildren && Child != nullptr)
|
|
{
|
|
Child->SetDiffuseOverride(Color, Includechildren, true); // node's children include child's siblings and children
|
|
}
|
|
}
|
|
|
|
std::optional<glm::vec3> TSubModel::GetDiffuse(float Includesiblings)
|
|
{
|
|
if (eType == TP_FREESPOTLIGHT)
|
|
{
|
|
if (DiffuseOverride.x >= 0.0f)
|
|
return DiffuseOverride;
|
|
else
|
|
return glm::vec3(f4Diffuse);
|
|
}
|
|
|
|
if (Includesiblings)
|
|
{
|
|
auto sibling = this;
|
|
while ((sibling = sibling->Next))
|
|
{
|
|
auto result = sibling->GetDiffuse(true);
|
|
if (result)
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if (Child)
|
|
return Child->GetDiffuse(true);
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
// sets visibility level (alpha component) to specified value
|
|
void TSubModel::SetVisibilityLevel(float const Level, bool const Includechildren, bool const Includesiblings)
|
|
{
|
|
|
|
fVisible = Level;
|
|
if (true == Includesiblings)
|
|
{
|
|
auto sibling{this};
|
|
while ((sibling = sibling->Next) != nullptr)
|
|
{
|
|
sibling->SetVisibilityLevel(Level, Includechildren, false); // no need for all siblings to duplicate the work
|
|
}
|
|
}
|
|
if (true == Includechildren && Child != nullptr)
|
|
{
|
|
Child->SetVisibilityLevel(Level, Includechildren, true); // node's children include child's siblings and children
|
|
}
|
|
}
|
|
|
|
// sets light level (alpha component of illumination color) to specified value
|
|
void TSubModel::SetLightLevel(glm::vec4 const &Level, bool const Includechildren, bool const Includesiblings)
|
|
{
|
|
/*
|
|
f4Emision = Level;
|
|
*/
|
|
f4Diffuse = {Level.r, Level.g, Level.b, f4Diffuse.a};
|
|
f4Emision.a = Level.a;
|
|
if (true == Includesiblings)
|
|
{
|
|
auto sibling{this};
|
|
while ((sibling = sibling->Next) != nullptr)
|
|
{
|
|
sibling->SetLightLevel(Level, Includechildren, false); // no need for all siblings to duplicate the work
|
|
}
|
|
}
|
|
if (true == Includechildren && Child != nullptr)
|
|
{
|
|
Child->SetLightLevel(Level, Includechildren, true); // node's children include child's siblings and children
|
|
}
|
|
}
|
|
|
|
// sets activation threshold of self-illumination to specitied value
|
|
void TSubModel::SetSelfIllum(float const Threshold, bool const Includechildren, bool const Includesiblings)
|
|
{
|
|
|
|
fLight = Threshold;
|
|
if (true == Includesiblings)
|
|
{
|
|
auto sibling{this};
|
|
while ((sibling = sibling->Next) != nullptr)
|
|
{
|
|
sibling->SetSelfIllum(Threshold, Includechildren, false); // no need for all siblings to duplicate the work
|
|
}
|
|
}
|
|
if (true == Includechildren && Child != nullptr)
|
|
{
|
|
Child->SetSelfIllum(Threshold, Includechildren, true); // node's children include child's siblings and children
|
|
}
|
|
}
|
|
|
|
int TSubModel::SeekFaceNormal(std::vector<unsigned int> const &Masks, int const Startface, unsigned int const Mask, glm::vec3 const &Position, gfx::vertex_array const &Vertices)
|
|
{ // szukanie punktu stycznego do (pt), zwraca numer wierzchołka, a nie trójkąta
|
|
int facecount = m_geometry.vertex_count / 3;
|
|
for (int faceidx = Startface; faceidx < facecount; ++faceidx)
|
|
{
|
|
// pętla po trójkątach, od trójkąta (f)
|
|
if (Masks[faceidx] & Mask)
|
|
{
|
|
// jeśli wspólna maska powierzchni
|
|
for (int vertexidx = 0; vertexidx < 3; ++vertexidx)
|
|
{
|
|
if (Vertices[3 * faceidx + vertexidx].position == Position)
|
|
{
|
|
return 3 * faceidx + vertexidx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1; // nie znaleziono stycznego wierzchołka
|
|
}
|
|
|
|
float emm1[] = {1, 1, 1, 0};
|
|
float emm2[] = {0, 0, 0, 1};
|
|
|
|
inline void readColor(cParser &parser, glm::vec4 &color)
|
|
{
|
|
int discard;
|
|
parser.getTokens(4, false);
|
|
parser >> discard >> color.r >> color.g >> color.b;
|
|
color /= 255.0f;
|
|
};
|
|
|
|
inline void readMatrix(cParser &parser, float4x4 &matrix)
|
|
{ // Ra: wczytanie transforma
|
|
parser.getTokens(16, false);
|
|
for (int x = 0; x <= 3; ++x) // wiersze
|
|
for (int y = 0; y <= 3; ++y) // kolumny
|
|
parser >> matrix(x)[y];
|
|
};
|
|
|
|
template <typename T> void UserdataParse(cParser &parser, gfx::vertex_userdata &vertex)
|
|
{
|
|
static_assert(sizeof(T) == sizeof(float), "userdata is stored as raw 32-bit values");
|
|
parser.getTokens(4);
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
T value{};
|
|
parser >> value;
|
|
vertex.data[i] = std::bit_cast<float>(value);
|
|
}
|
|
}
|
|
|
|
std::pair<int, int> TSubModel::Load(cParser &parser, bool dynamic)
|
|
{ // Ra: VBO tworzone na poziomie modelu, a nie submodeli
|
|
auto token{parser.getToken<std::string>()};
|
|
if (token != "type:")
|
|
{
|
|
std::string errormessage{"Bad model: expected submodel type definition not found while loading model \"" + parser.Name() + "\"" + "\ncurrent model data stream content: \""};
|
|
auto count{10};
|
|
while (true == parser.getTokens() && false == (token = parser.peek()).empty() && token != "parent:")
|
|
{
|
|
// skip data until next submodel, dump first few tokens in the error message
|
|
if (--count > 0)
|
|
{
|
|
errormessage += token + " ";
|
|
}
|
|
}
|
|
errormessage += "(...)\"";
|
|
ErrorLog(errormessage);
|
|
return {0, 0};
|
|
}
|
|
{
|
|
auto const type{parser.getToken<std::string>()};
|
|
if (type == "mesh")
|
|
eType = GL_TRIANGLES; // submodel - trójkaty
|
|
else if (type == "point")
|
|
eType = GL_POINTS; // co to niby jest?
|
|
else if (type == "freespotlight")
|
|
eType = TP_FREESPOTLIGHT; // światełko
|
|
else if (type == "text")
|
|
eType = TP_TEXT; // wyświetlacz tekstowy (generator napisów)
|
|
else if (type == "stars")
|
|
eType = TP_STARS; // wiele punktów świetlnych
|
|
};
|
|
parser.ignoreToken();
|
|
parser.getTokens(1, false); // nazwa submodelu bez zmieny na małe
|
|
parser >> token;
|
|
Name(token);
|
|
if (dynamic)
|
|
{
|
|
// dla pojazdu, blokujemy załączone submodele, które mogą być nieobsługiwane
|
|
if (token.size() >= 3 && token.find("_on") + 3 == token.length())
|
|
{
|
|
// jeśli nazwa kończy się na "_on" to domyślnie wyłączyć, żeby się nie nakładało z obiektem "_off"
|
|
iVisible = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// dla pozostałych modeli blokujemy zapalone światła, które mogą być nieobsługiwane
|
|
if (token.compare(0, 8, "Light_On") == 0)
|
|
{
|
|
// jeśli nazwa zaczyna się od "Light_On" to domyślnie wyłączyć, żeby się nie nakładało z obiektem "Light_Off"
|
|
iVisible = 0;
|
|
}
|
|
}
|
|
|
|
if (parser.expectToken("anim:")) // Ra: ta informacja by się przydała!
|
|
{ // rodzaj animacji
|
|
std::string type = parser.getToken<std::string>();
|
|
if (type != "false")
|
|
{
|
|
iFlags |= 0x4000; // jak animacja, to trzeba przechowywać macierz zawsze
|
|
if (type == "seconds_jump")
|
|
b_Anim = b_aAnim = TAnimType::at_SecondsJump; // sekundy z przeskokiem
|
|
else if (type == "minutes_jump")
|
|
b_Anim = b_aAnim = TAnimType::at_MinutesJump; // minuty z przeskokiem
|
|
else if (type == "hours_jump")
|
|
b_Anim = b_aAnim = TAnimType::at_HoursJump; // godziny z przeskokiem
|
|
else if (type == "hours24_jump")
|
|
b_Anim = b_aAnim = TAnimType::at_Hours24Jump; // godziny z przeskokiem
|
|
else if (type == "seconds")
|
|
b_Anim = b_aAnim = TAnimType::at_Seconds; // minuty płynnie
|
|
else if (type == "minutes")
|
|
b_Anim = b_aAnim = TAnimType::at_Minutes; // minuty płynnie
|
|
else if (type == "hours")
|
|
b_Anim = b_aAnim = TAnimType::at_Hours; // godziny płynnie
|
|
else if (type == "hours24")
|
|
b_Anim = b_aAnim = TAnimType::at_Hours24; // godziny płynnie
|
|
else if (type == "billboard")
|
|
b_Anim = b_aAnim = TAnimType::at_Billboard; // obrót w pionie do kamery
|
|
else if (type == "wind")
|
|
b_Anim = b_aAnim = TAnimType::at_Wind; // ruch pod wpływem wiatru
|
|
else if (type == "sky")
|
|
b_Anim = b_aAnim = TAnimType::at_Sky; // aniamacja nieba
|
|
else if (type == "digital")
|
|
b_Anim = b_aAnim = TAnimType::at_Digital; // licznik mechaniczny
|
|
else if (type == "digiclk")
|
|
b_Anim = b_aAnim = TAnimType::at_DigiClk; // zegar cyfrowy
|
|
else
|
|
b_Anim = b_aAnim = TAnimType::at_Undefined; // nieznana forma animacji
|
|
}
|
|
}
|
|
if (eType < TP_ROTATOR)
|
|
readColor(parser, f4Ambient); // ignoruje token przed
|
|
readColor(parser, f4Diffuse);
|
|
if (eType < TP_ROTATOR)
|
|
{
|
|
readColor(parser, f4Specular);
|
|
if (pName == "cien")
|
|
{
|
|
// crude workaround to kill specular on shadow geometry of legacy models
|
|
f4Specular = glm::vec4{0.0f, 0.0f, 0.0f, 1.0f};
|
|
}
|
|
}
|
|
parser.ignoreToken(); // zignorowanie nazwy "SelfIllum:"
|
|
{
|
|
std::string light = parser.getToken<std::string>();
|
|
if (light == "true")
|
|
fLight = 2.0; // zawsze świeci
|
|
else if (light == "false")
|
|
fLight = -1.0; // zawsze ciemy
|
|
else
|
|
fLight = std::stod(light);
|
|
};
|
|
if (eType == TP_FREESPOTLIGHT)
|
|
{
|
|
if (!parser.expectToken("nearattenstart:"))
|
|
{
|
|
Error("Model light parse failure!");
|
|
}
|
|
std::string discard;
|
|
discard.reserve(64);
|
|
parser.getTokens(13, false);
|
|
parser >> fNearAttenStart >> discard >> fNearAttenEnd >> discard >> bUseNearAtten >> discard >> iFarAttenDecay >> discard >> fFarDecayRadius >> discard >>
|
|
fCosFalloffAngle // kąt liczony dla średnicy, a nie promienia
|
|
>> discard >> fCosHotspotAngle; // kąt liczony dla średnicy, a nie promienia
|
|
// trzeba pobrac do kolejki 2 tokeny
|
|
parser.getTokens(2);
|
|
if (parser.peek() == "hotspotpower:")
|
|
{
|
|
float multiplier;
|
|
parser >> discard >> multiplier;
|
|
WriteLog("Found hotspot power!");
|
|
// recaluclate multiplier
|
|
diffuseMultiplier = multiplier / 100.f;
|
|
|
|
// apply color only on nvrenderer
|
|
if (Global.NvRenderer)
|
|
f4Diffuse *= diffuseMultiplier;
|
|
}
|
|
else
|
|
parser.autoclear(false);
|
|
|
|
// convert conve parameters if specified in degrees
|
|
if (fCosFalloffAngle > 1.0)
|
|
{
|
|
fCosFalloffAngle = std::cos(glm::radians(0.5f * fCosFalloffAngle));
|
|
}
|
|
if (fCosHotspotAngle > 1.0)
|
|
{
|
|
fCosHotspotAngle = std::cos(glm::radians(0.5f * fCosHotspotAngle));
|
|
}
|
|
m_geometry.vertex_count = 1;
|
|
iFlags |= 0x4030; // drawn both in solid (light point) and transparent (light glare) phases
|
|
}
|
|
else if (eType < TP_ROTATOR)
|
|
{
|
|
std::string discard;
|
|
parser.getTokens(6, false);
|
|
parser >> discard >> bWire >> discard >> fWireSize >> discard >> Opacity;
|
|
// wymagane jest 0 dla szyb, 100 idzie w nieprzezroczyste
|
|
if (Opacity > 1.f)
|
|
{
|
|
Opacity = std::min(1.f, Opacity * 0.01f);
|
|
}
|
|
if (Opacity < -1.f)
|
|
{
|
|
Opacity = std::max(-1.f, Opacity * 0.01f);
|
|
}
|
|
|
|
if (!parser.expectToken("map:"))
|
|
Error("Model map parse failure!");
|
|
std::string material = parser.getToken<std::string>();
|
|
std::replace(material.begin(), material.end(), '\\', '/');
|
|
if (material == "none")
|
|
{ // rysowanie podanym kolorem
|
|
Name_Material("colored");
|
|
m_material = GfxRenderer->Fetch_Material(m_materialname);
|
|
iFlags |= 0x10; // rysowane w cyklu nieprzezroczystych
|
|
}
|
|
else if (material.find("replacableskin") != material.npos)
|
|
{ // McZapkie-060702: zmienialne skory modelu
|
|
m_material = -1;
|
|
iFlags |= Opacity < 0.999 ? 1 : 0x10; // zmienna tekstura 1
|
|
}
|
|
else if (material == "-1")
|
|
{
|
|
m_material = -1;
|
|
iFlags |= Opacity < 0.999 ? 1 : 0x10; // zmienna tekstura 1
|
|
}
|
|
else if (material == "-2")
|
|
{
|
|
m_material = -2;
|
|
iFlags |= Opacity < 0.999 ? 2 : 0x10; // zmienna tekstura 2
|
|
}
|
|
else if (material == "-3")
|
|
{
|
|
m_material = -3;
|
|
iFlags |= Opacity < 0.999 ? 4 : 0x10; // zmienna tekstura 3
|
|
}
|
|
else if (material == "-4")
|
|
{
|
|
m_material = -4;
|
|
iFlags |= Opacity < 0.999 ? 8 : 0x10; // zmienna tekstura 4
|
|
}
|
|
else
|
|
{
|
|
Name_Material(material);
|
|
/*
|
|
if( material.find_first_of( "/" ) == material.npos ) {
|
|
// jeśli tylko nazwa pliku, to dawać bieżącą ścieżkę do tekstur
|
|
material.insert( 0, Global.asCurrentTexturePath );
|
|
}
|
|
*/
|
|
m_material = GfxRenderer->Fetch_Material(material);
|
|
// renderowanie w cyklu przezroczystych tylko jeśli:
|
|
// 1. Opacity=0 (przejściowo <1, czy tam <100)
|
|
iFlags |= Opacity < 0.999f ? 0x20 : 0x10; // 0x20-przezroczysta, 0x10-nieprzezroczysta
|
|
};
|
|
}
|
|
else if (eType == TP_STARS)
|
|
{
|
|
m_material = GfxRenderer->Fetch_Material("stars");
|
|
iFlags |= 0x10;
|
|
}
|
|
else
|
|
{
|
|
iFlags |= 0x10;
|
|
}
|
|
|
|
if (m_material > 0)
|
|
{
|
|
const IMaterial *mat = GfxRenderer->Material(m_material);
|
|
/*
|
|
// if material does have opacity set, replace submodel opacity with it
|
|
if (mat.opacity)
|
|
{
|
|
iFlags &= ~0x30;
|
|
if (*mat.opacity == 0.0f)
|
|
iFlags |= 0x20; // translucent
|
|
else
|
|
iFlags |= 0x10; // opaque
|
|
}
|
|
*/
|
|
// and same thing with selfillum
|
|
if (mat->GetSelfillum())
|
|
fLight = *mat->GetSelfillum();
|
|
}
|
|
|
|
// visibility range
|
|
std::string discard;
|
|
parser.getTokens(5, false);
|
|
parser.autoclear(true);
|
|
parser >> discard >> fSquareMaxDist >> discard >> fSquareMinDist >> discard;
|
|
|
|
if (fSquareMaxDist <= 0.0)
|
|
{
|
|
// 15km to więcej, niż się obecnie wyświetla
|
|
fSquareMaxDist = 15000.0;
|
|
}
|
|
fSquareMaxDist *= fSquareMaxDist;
|
|
fSquareMinDist *= fSquareMinDist;
|
|
|
|
// transformation matrix
|
|
fMatrix = new float4x4();
|
|
readMatrix(parser, *fMatrix); // wczytanie transform
|
|
if (Parent != nullptr)
|
|
transformscalestack = Parent->transformscalestack;
|
|
if (!fMatrix->IdentityIs())
|
|
{
|
|
iFlags |= 0x8000; // transform niejedynkowy - trzeba go przechować
|
|
// check the scaling
|
|
auto const matrix = glm::make_mat4(fMatrix->readArray());
|
|
glm::vec3 const scale{glm::length(glm::vec3(glm::column(matrix, 0))), glm::length(glm::vec3(glm::column(matrix, 1))), glm::length(glm::vec3(glm::column(matrix, 2)))};
|
|
if (std::abs(scale.x - 1.0f) > 0.01 || std::abs(scale.y - 1.0f) > 0.01 || std::abs(scale.z - 1.0f) > 0.01)
|
|
{
|
|
ErrorLog("Bad model: transformation matrix for sub-model \"" + pName + "\" imposes geometry scaling (factors: " + to_string(scale) + ")", logtype::model);
|
|
m_normalizenormals = std::abs(scale.x - scale.y) < 0.01f && std::abs(scale.y - scale.z) < 0.01f ? rescale : normalize;
|
|
}
|
|
transformscalestack *= (scale.x + scale.y + scale.z) / 3.0f;
|
|
}
|
|
if (eType < TP_ROTATOR)
|
|
{ // wczytywanie wierzchołków
|
|
token = parser.getToken<std::string>();
|
|
bool has_userdata = false;
|
|
if (token == "userdata:")
|
|
{
|
|
has_userdata = parser.getToken<bool>();
|
|
|
|
token = parser.getToken<std::string>();
|
|
}
|
|
if (token == "numindices:") // optional block, potentially preceeding vertex list
|
|
{
|
|
m_geometry.index_count = parser.getToken<int>(false);
|
|
Indices.resize(m_geometry.index_count);
|
|
for (auto idx = 0; idx < m_geometry.index_count; ++idx)
|
|
{
|
|
Indices[idx] = parser.getToken<unsigned int>(false);
|
|
}
|
|
token = parser.getToken<std::string>();
|
|
}
|
|
if (token == "numverts:" || token == "numverts")
|
|
{ // normalna lista wierzchołków
|
|
/*
|
|
// Ra 15-01: to wczytać jako tekst - jeśli pierwszy znak zawiera "*", to
|
|
// dalej będzie nazwa wcześniejszego submodelu, z którego należy wziąć wierzchołki
|
|
// zapewni to jakąś zgodność wstecz, bo zamiast liczby będzie ciąg, którego
|
|
// wartość powinna być uznana jako zerowa
|
|
token = parser.getToken<std::string>();
|
|
if( token.front() == '*' ) {
|
|
// jeśli pierwszy znak jest gwiazdką, poszukać submodelu o nazwie bez tej gwiazdki i wziąć z niego wierzchołki
|
|
Error( "Vertices reference not yet supported!" );
|
|
}
|
|
*/
|
|
m_geometry.vertex_count = parser.getToken<int>(false);
|
|
if (m_geometry.index_count <= 0 && m_geometry.vertex_count % 3 != 0)
|
|
{
|
|
m_geometry.vertex_count = 0;
|
|
Error("Bad model: incomplete triangle encountered in submodel \"" + pName + "\"");
|
|
return {0, 0};
|
|
}
|
|
if (m_geometry.vertex_count)
|
|
{
|
|
Vertices.resize(m_geometry.vertex_count);
|
|
if (m_geometry.index_count)
|
|
{
|
|
// indexed geometry chunks are expected to come with pre-generated normals and tangents, to avoid hassle required to generate them manually
|
|
auto vertices{std::begin(Vertices)};
|
|
for (auto idx = 0; idx < m_geometry.vertex_count; ++idx)
|
|
{
|
|
auto vertex{vertices + idx};
|
|
parser.getTokens(3 + 3 + 2 + 4, false);
|
|
parser >> vertex->position.x >> vertex->position.y >> vertex->position.z >> vertex->normal.x >> vertex->normal.y >> vertex->normal.z >> vertex->texture.s >>
|
|
vertex->texture.t >> vertex->tangent.x >> vertex->tangent.y >> vertex->tangent.z >> vertex->tangent.w;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// legacy geometry, more or less incomplete
|
|
// to stay on the safe side we'll potentially need to convert indexed geometry to raw triangle form, cal
|
|
auto vertices{std::begin(Vertices)};
|
|
int facecount = m_geometry.vertex_count / 3;
|
|
std::vector<unsigned int> sg;
|
|
sg.resize(facecount); // maski przynależności trójkątów do powierzchni
|
|
std::vector<int> wsp;
|
|
wsp.resize(m_geometry.vertex_count); // z którego wierzchołka kopiować wektor normalny
|
|
int maska = 0;
|
|
int vertexidx = 0; // used to keep track of vertex indices in source file
|
|
for (auto idx = 0; idx < m_geometry.vertex_count; ++idx)
|
|
{
|
|
++vertexidx;
|
|
// Ra: z konwersją na układ scenerii - będzie wydajniejsze wyświetlanie
|
|
wsp[idx] = -1; // wektory normalne nie są policzone dla tego wierzchołka
|
|
if (idx % 3 == 0)
|
|
{
|
|
// jeśli będzie maska -1, to dalej będą wierzchołki z wektorami normalnymi, podanymi jawnie
|
|
maska = parser.getToken<int>(false); // maska powierzchni trójkąta
|
|
// dla maski -1 będzie 0, czyli nie ma wspólnych wektorów normalnych
|
|
sg[idx / 3] = maska == -1 ? 0 : maska;
|
|
}
|
|
auto vertex{vertices + idx};
|
|
parser.getTokens(3, false);
|
|
parser >> vertex->position.x >> vertex->position.y >> vertex->position.z;
|
|
if (maska == -1)
|
|
{ // jeśli wektory normalne podane jawnie
|
|
parser.getTokens(3, false);
|
|
parser >> vertex->normal.x >> vertex->normal.y >> vertex->normal.z;
|
|
if (glm::length2(vertex->normal) > 0.0f)
|
|
{
|
|
vertex->normal = glm::normalize(vertex->normal);
|
|
}
|
|
else
|
|
{
|
|
WriteLog("Bad model: zero length normal vector specified in submodel \"" + pName + "\", vertex " + std::to_string(idx), logtype::model);
|
|
}
|
|
wsp[idx] = idx; // wektory normalne "są już policzone"
|
|
}
|
|
parser.getTokens(2, false);
|
|
parser >> vertex->texture.s >> vertex->texture.t;
|
|
|
|
if (idx % 3 == 2)
|
|
{
|
|
// jeżeli wczytano 3 punkty
|
|
if (true == degenerate(vertex->position, (vertex - 1)->position, (vertex - 2)->position))
|
|
{
|
|
// jeżeli punkty się nakładają na siebie
|
|
--facecount; // o jeden trójkąt mniej
|
|
m_geometry.vertex_count -= 3; // czyli o 3 wierzchołki
|
|
idx -= 3; // wczytanie kolejnego w to miejsce
|
|
WriteLog("Bad model: degenerated triangle ignored in: \"" + pName + "\", vertices " + std::to_string(vertexidx - 2) + "-" + std::to_string(vertexidx), logtype::model);
|
|
}
|
|
if (idx > 0)
|
|
{
|
|
// jeśli pierwszy trójkąt będzie zdegenerowany, to zostanie usunięty i nie ma co sprawdzać
|
|
// length2 is better than length for comparing because it does not require sqrt function
|
|
if (glm::length2(vertex->position - (vertex - 1)->position) > sq(1000.0) || glm::length2((vertex - 1)->position - (vertex - 2)->position) > sq(1000.0) ||
|
|
glm::length2((vertex - 2)->position - vertex->position) > sq(1000.0))
|
|
{
|
|
// jeżeli są dalej niż 2km od siebie //Ra 15-01:
|
|
// obiekt wstawiany nie powinien być większy niż 300m (trójkąty terenu w E3D mogą mieć 1.5km)
|
|
--facecount; // o jeden trójkąt mniej
|
|
m_geometry.vertex_count -= 3; // czyli o 3 wierzchołki
|
|
idx -= 3; // wczytanie kolejnego w to miejsce
|
|
WriteLog("Bad model: too large triangle ignored in: \"" + pName + "\"", logtype::model);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<glm::vec3> facenormals;
|
|
facenormals.reserve(facecount);
|
|
for (int i = 0; i < facecount; ++i)
|
|
{
|
|
// pętla po trójkątach - będzie szybciej, jak wstępnie przeliczymy normalne trójkątów
|
|
auto const vertex{vertices + i * 3};
|
|
auto facenormal = glm::cross(vertex->position - (vertex + 1)->position, vertex->position - (vertex + 2)->position);
|
|
facenormals.emplace_back(glm::length2(facenormal) > 0.0f ? glm::normalize(facenormal) : glm::vec3());
|
|
}
|
|
glm::vec3 vertexnormal; // roboczy wektor normalny
|
|
for (int vertexidx = 0; vertexidx < m_geometry.vertex_count; ++vertexidx)
|
|
{
|
|
// pętla po wierzchołkach trójkątów
|
|
auto vertex{vertices + vertexidx};
|
|
if (wsp[vertexidx] >= 0)
|
|
{
|
|
// jeśli już był liczony wektor normalny z użyciem tego wierzchołka to wystarczy skopiować policzony wcześniej
|
|
vertex->normal = (vertices + wsp[vertexidx])->normal;
|
|
}
|
|
else
|
|
{
|
|
// inaczej musimy dopiero policzyć
|
|
auto const faceidx = vertexidx / 3; // numer trójkąta
|
|
vertexnormal = glm::vec3(); // liczenie zaczynamy od zera
|
|
auto adjacenvertextidx = vertexidx; // zaczynamy dodawanie wektorów normalnych od własnego
|
|
while (adjacenvertextidx >= 0)
|
|
{
|
|
// sumowanie z wektorem normalnym sąsiada (włącznie ze sobą)
|
|
if (glm::dot(vertexnormal, facenormals[adjacenvertextidx / 3]) > -0.99f)
|
|
{
|
|
wsp[adjacenvertextidx] = vertexidx; // informacja, że w tym wierzchołku jest już policzony wektor normalny
|
|
vertexnormal += facenormals[adjacenvertextidx / 3];
|
|
}
|
|
/*
|
|
else {
|
|
ErrorLog( "Bad model: opposite normals in the same smoothing group, check sub-model \"" + pName + "\" for two-sided faces and/or scaling", logtype::model );
|
|
}
|
|
*/
|
|
// i szukanie od kolejnego trójkąta
|
|
adjacenvertextidx = SeekFaceNormal(sg, adjacenvertextidx / 3 + 1, sg[faceidx], vertex->position, Vertices);
|
|
}
|
|
// Ra 15-01: należało by jeszcze uwzględnić skalowanie wprowadzane przez transformy, aby normalne po przeskalowaniu były jednostkowe
|
|
if (glm::length2(vertexnormal) == 0.0f)
|
|
{
|
|
WriteLog("Bad model: zero length normal vector generated for sub-model \"" + pName + "\"", logtype::model);
|
|
}
|
|
vertex->normal = glm::length2(vertexnormal) > 0.0f ? glm::normalize(vertexnormal) : facenormals[vertexidx / 3]; // przepisanie do wierzchołka trójkąta
|
|
}
|
|
}
|
|
Vertices.resize(m_geometry.vertex_count); // in case we had some degenerate triangles along the way
|
|
gfx::calculate_indices(Indices, Vertices, Userdata, transformscalestack);
|
|
gfx::calculate_tangents(Vertices, Indices, GL_TRIANGLES);
|
|
// update values potentially changed by indexing
|
|
m_geometry.index_count = Indices.size();
|
|
m_geometry.vertex_count = Vertices.size();
|
|
}
|
|
if (has_userdata)
|
|
{
|
|
Userdata.resize(m_geometry.vertex_count);
|
|
auto userdata{std::begin(Userdata)};
|
|
for (auto idx = 0; idx < m_geometry.vertex_count; ++idx)
|
|
{
|
|
auto vertex{userdata + idx};
|
|
UserdataParse<float>(parser, *vertex);
|
|
}
|
|
}
|
|
}
|
|
else // gdy brak wierzchołków
|
|
{
|
|
eType = TP_ROTATOR; // submodel pomocniczy, ma tylko macierz przekształcenia
|
|
// dla formalności
|
|
m_geometry.vertex_offset = 0;
|
|
m_geometry.vertex_count = 0;
|
|
}
|
|
} // obsługa submodelu z własną listą wierzchołków
|
|
}
|
|
else if (eType == TP_STARS)
|
|
{ // punkty świecące dookólnie - składnia jak
|
|
// dla smt_Mesh
|
|
std::string discard;
|
|
parser.getTokens(2, false);
|
|
parser >> discard >> m_geometry.vertex_count;
|
|
Vertices.resize(m_geometry.vertex_count);
|
|
int idx;
|
|
unsigned int color;
|
|
auto vertices{std::begin(Vertices)};
|
|
for (idx = 0; idx < m_geometry.vertex_count; ++idx)
|
|
{
|
|
if (idx % 3 == 0)
|
|
{
|
|
parser.ignoreToken(); // maska powierzchni trójkąta
|
|
}
|
|
auto vertex{vertices + idx};
|
|
parser.getTokens(5, false);
|
|
parser >> vertex->position.x >> vertex->position.y >> vertex->position.z >> color // zakodowany kolor
|
|
>> discard;
|
|
vertex->normal = {(color & 0xff) / 255.0f, // R
|
|
(color >> 8 & 0xff) / 255.0f, // G
|
|
(color >> 16 & 0xff) / 255.0f}; // B
|
|
}
|
|
}
|
|
else if (eType == TP_FREESPOTLIGHT)
|
|
{
|
|
// single light points only have single data point, duh
|
|
Vertices.emplace_back();
|
|
m_geometry.vertex_count = 1;
|
|
}
|
|
// Visible=true; //się potem wyłączy w razie potrzeby
|
|
// iFlags|=0x0200; //wczytano z pliku tekstowego (jest właścicielem tablic)
|
|
if (m_geometry.vertex_count < 1)
|
|
iFlags &= ~0x3F; // cykl renderowania uzależniony od potomnych
|
|
|
|
return {m_geometry.index_count, m_geometry.vertex_count}; // do określenia wielkości VBO
|
|
};
|
|
|
|
/*
|
|
int TSubModel::TriangleAdd(TModel3d *m, material_handle tex, int tri)
|
|
{ // dodanie trójkątów do submodelu, używane przy tworzeniu E3D terenu
|
|
TSubModel *s = this;
|
|
while (s ? (s->m_material != tex) : false)
|
|
{ // szukanie submodelu o danej teksturze
|
|
if (s == this)
|
|
s = Child;
|
|
else
|
|
s = s->Next;
|
|
}
|
|
if (!s)
|
|
{
|
|
if (m_material <= 0)
|
|
s = this; // użycie głównego
|
|
else
|
|
{ // dodanie nowego submodelu do listy potomnych
|
|
s = new TSubModel();
|
|
m->AddTo(this, s);
|
|
}
|
|
s->Name_Material(GfxRenderer->Material(tex).name);
|
|
s->m_material = tex;
|
|
s->eType = GL_TRIANGLES;
|
|
}
|
|
if (s->iNumVerts < 0)
|
|
s->iNumVerts = tri; // bo na początku jest -1, czyli że nie wiadomo
|
|
else
|
|
s->iNumVerts += tri; // aktualizacja ilości wierzchołków
|
|
return s->iNumVerts - tri; // zwraca pozycję tych trójkątów w submodelu
|
|
};
|
|
*/
|
|
/*
|
|
basic_vertex *TSubModel::TrianglePtr(int tex, int pos, glm::vec3 const &Ambient, glm::vec3 const &Diffuse, glm::vec3 const &Specular )
|
|
{ // zwraca wskaźnik do wypełnienia tabeli wierzchołków, używane przy tworzeniu E3D terenu
|
|
|
|
TSubModel *s = this;
|
|
while (s ? s->TextureID != tex : false)
|
|
{ // szukanie submodelu o danej teksturze
|
|
if (s == this)
|
|
s = Child;
|
|
else
|
|
s = s->Next;
|
|
}
|
|
if (!s)
|
|
return NULL; // coś nie tak poszło
|
|
if (!s->Vertices)
|
|
{ // utworznie tabeli trójkątów
|
|
s->Vertices = new basic_vertex[s->iNumVerts];
|
|
s->iVboPtr = iInstance; // pozycja submodelu w tabeli wierzchołków
|
|
iInstance += s->iNumVerts; // pozycja dla następnego
|
|
}
|
|
s->ColorsSet(Ambient, Diffuse, Specular); // ustawienie kolorów świateł
|
|
return s->Vertices + pos; // wskaźnik na wolne miejsce w tabeli wierzchołków
|
|
};
|
|
*/
|
|
|
|
void TSubModel::InitialRotate(bool doit)
|
|
{ // konwersja układu współrzędnych na zgodny ze scenerią
|
|
if (iFlags & 0xC000) // jeśli jest animacja albo niejednostkowy transform
|
|
{ // niejednostkowy transform jest mnożony i wystarczy zabawy
|
|
if (doit)
|
|
{
|
|
// obrót lewostronny
|
|
if (!fMatrix)
|
|
{
|
|
// macierzy może nie być w dodanym "bananie"
|
|
fMatrix = new float4x4(); // tworzy macierz o przypadkowej zawartości
|
|
fMatrix->Identity(); // a zaczynamy obracanie od jednostkowej
|
|
}
|
|
iFlags |= 0x8000; // po obróceniu będzie raczej niejedynkowy matrix
|
|
fMatrix->InitialRotate(); // zmiana znaku X oraz zamiana Y i Z
|
|
if (fMatrix->IdentityIs())
|
|
iFlags &= ~0x8000; // jednak jednostkowa po obróceniu
|
|
}
|
|
if (Child)
|
|
{
|
|
// potomnych nie obracamy już, tylko ewentualnie optymalizujemy
|
|
Child->InitialRotate(false);
|
|
}
|
|
else if (Global.iConvertModels & 2)
|
|
{
|
|
// optymalizacja jest opcjonalna
|
|
if ((iFlags & 0xC000) == 0x8000 // o ile nie ma animacji
|
|
&& false == is_emitter()) // don't optimize smoke emitter attachment points
|
|
{ // jak nie ma potomnych, można wymnożyć przez transform i wyjedynkować go
|
|
float4x4 *mat = GetMatrix(); // transform submodelu
|
|
if (false == Vertices.empty())
|
|
{
|
|
for (auto &vertex : Vertices)
|
|
{
|
|
vertex.position = *mat * vertex.position;
|
|
}
|
|
// zerujemy przesunięcie przed obracaniem normalnych
|
|
(*mat)(3)[0] = (*mat)(3)[1] = (*mat)(3)[2] = 0.0;
|
|
if (eType != TP_STARS)
|
|
{
|
|
// gwiazdki mają kolory zamiast normalnych, to ich wtedy nie ruszamy
|
|
for (auto &vertex : Vertices)
|
|
{
|
|
vertex.normal = *mat * vertex.normal;
|
|
vertex.tangent.xyz = *mat * vertex.tangent.xyz;
|
|
}
|
|
}
|
|
}
|
|
mat->Identity(); // jedynkowanie transformu po przeliczeniu wierzchołków
|
|
iFlags &= ~0x8000; // transform jedynkowy
|
|
}
|
|
}
|
|
}
|
|
else // jak jest jednostkowy i nie ma animacji
|
|
if (doit)
|
|
{ // jeśli jest jednostkowy transform, to przeliczamy
|
|
// wierzchołki, a mnożenie podajemy dalej
|
|
for (auto &vertex : Vertices)
|
|
{
|
|
glm::mat4 vertexTransform{{-1.f, 0.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}, {0.f, 1.f, 0.f, 0.f}, {0.f, 0.f, 0.f, 1.f}};
|
|
vertex.position = vertexTransform * glm::vec4(vertex.position, 1.f);
|
|
// wektory normalne również trzeba przekształcić, bo się źle oświetlają
|
|
if (eType != TP_STARS)
|
|
{
|
|
glm::mat3 normalTransform{{-1.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, {0.f, 1.f, 0.f}};
|
|
// gwiazdki mają kolory zamiast normalnych, to // ich wtedy nie ruszamy
|
|
vertex.normal = normalTransform * vertex.normal;
|
|
vertex.tangent.xyz = normalTransform * vertex.tangent.xyz;
|
|
}
|
|
}
|
|
if (Child)
|
|
Child->InitialRotate(doit); // potomne ewentualnie obrócimy
|
|
}
|
|
if (Next)
|
|
Next->InitialRotate(doit);
|
|
};
|
|
|
|
// text display (TP_TEXT): returns sub-models of the characters to draw for provided text, in drawing order.
|
|
// a character is matched with the first character of a child sub-model name. both the text and the names can be
|
|
// encoded either in utf-8 or in windows-1250, so the legacy (ansi) scenery and model files work along the utf-8 ones
|
|
std::vector<TSubModel *> const &TSubModel::text_letters(std::string const &Text)
|
|
{
|
|
// the sequence is prepared once for each text the display shows. the texts of the model instances don't change,
|
|
// so drawing a display afterwards costs a single lookup, however many instances and passes take turns
|
|
auto const prepared{m_textlayouts.find(Text)};
|
|
if (prepared != m_textlayouts.end())
|
|
{
|
|
return prepared->second;
|
|
}
|
|
|
|
std::u32string characters;
|
|
if (false == m_lettersready)
|
|
{
|
|
for (auto *letter = Child; letter != nullptr; letter = letter->Next)
|
|
{
|
|
if (letter->pName.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (false == utf8_to_utf32(letter->pName, characters))
|
|
{
|
|
win1250_to_utf32(letter->pName, characters);
|
|
}
|
|
if (characters.front() != 0)
|
|
{
|
|
m_letters[characters.front()] = letter;
|
|
}
|
|
}
|
|
m_lettersready = true;
|
|
}
|
|
|
|
// the text is taken for utf-8 if it's valid as such, and for windows-1250 otherwise. a short ansi text can pass
|
|
// for utf-8 by accident (two upper case accented letters in a row are enough for this to happen),
|
|
// so if both readings are possible the one which finds more of its characters in the model wins
|
|
auto const known = [this](std::u32string const &Characters) {
|
|
return std::ranges::count_if(Characters, [this](char32_t const Character) { return m_letters.contains(Character); });
|
|
};
|
|
std::u32string ansi;
|
|
win1250_to_utf32(Text, ansi);
|
|
if (false == utf8_to_utf32(Text, characters) || known(ansi) > known(characters))
|
|
{
|
|
characters = ansi;
|
|
}
|
|
|
|
auto &textletters{m_textlayouts[Text]};
|
|
// NOTE: the first character of the text isn't displayed. the displays always worked this way, and the existing
|
|
// content relies on it
|
|
for (std::size_t idx = 1; idx < characters.size(); ++idx)
|
|
{
|
|
auto const lookup{m_letters.find(characters[idx])};
|
|
if (lookup != m_letters.end())
|
|
{
|
|
textletters.emplace_back(lookup->second);
|
|
}
|
|
}
|
|
return textletters;
|
|
}
|
|
|
|
void TSubModel::ChildAdd(TSubModel *SubModel)
|
|
{ // dodanie submodelu potemnego (uzależnionego)
|
|
// Ra: zmiana kolejności, żeby kolejne móc renderować po aktualnym (było
|
|
// przed)
|
|
if (SubModel)
|
|
SubModel->NextAdd(Child); // Ra: zmiana kolejności renderowania
|
|
Child = SubModel;
|
|
};
|
|
|
|
void TSubModel::NextAdd(TSubModel *SubModel)
|
|
{ // dodanie submodelu kolejnego (wspólny przodek)
|
|
if (Next)
|
|
Next->NextAdd(SubModel);
|
|
else
|
|
Next = SubModel;
|
|
};
|
|
|
|
int TSubModel::count_siblings()
|
|
{
|
|
|
|
auto siblingcount{0};
|
|
auto *sibling{Next};
|
|
while (sibling != nullptr)
|
|
{
|
|
++siblingcount;
|
|
sibling = sibling->Next;
|
|
}
|
|
return siblingcount;
|
|
}
|
|
|
|
int TSubModel::count_children()
|
|
{
|
|
|
|
return Child == nullptr ? 0 : 1 + Child->count_siblings();
|
|
}
|
|
|
|
// locates submodel mapped with replacable -4
|
|
std::tuple<TSubModel *, bool> TSubModel::find_replacable4()
|
|
{
|
|
|
|
if (m_material == -4)
|
|
{
|
|
return std::make_tuple(this, fLight != -1.0);
|
|
}
|
|
|
|
if (Next != nullptr)
|
|
{
|
|
auto lookup{Next->find_replacable4()};
|
|
if (std::get<TSubModel *>(lookup) != nullptr)
|
|
{
|
|
return lookup;
|
|
}
|
|
}
|
|
|
|
if (Child != nullptr)
|
|
{
|
|
auto lookup{Child->find_replacable4()};
|
|
if (std::get<TSubModel *>(lookup) != nullptr)
|
|
{
|
|
return lookup;
|
|
}
|
|
}
|
|
|
|
return std::make_tuple(nullptr, false);
|
|
}
|
|
|
|
// locates particle emitter submodels and adds them to provided list
|
|
void TSubModel::find_smoke_sources(nameoffset_sequence &Sourcelist) const
|
|
{
|
|
|
|
auto const name{ToLower(pName)};
|
|
|
|
if (eType == TP_ROTATOR && pName.find("smokesource_") == 0)
|
|
{
|
|
Sourcelist.emplace_back(pName, offset());
|
|
}
|
|
|
|
if (Next != nullptr)
|
|
{
|
|
Next->find_smoke_sources(Sourcelist);
|
|
}
|
|
|
|
if (Child != nullptr)
|
|
{
|
|
Child->find_smoke_sources(Sourcelist);
|
|
}
|
|
}
|
|
|
|
uint32_t TSubModel::FlagsCheck()
|
|
{ // analiza koniecznych zmian pomiędzy submodelami
|
|
// samo pomijanie glBindTexture() nie poprawi wydajności
|
|
// ale można sprawdzić, czy można w ogóle pominąć kod do tekstur (sprawdzanie
|
|
// replaceskin)
|
|
m_rotation_init_done = true;
|
|
|
|
uint32_t i = 0;
|
|
if (Child)
|
|
{ // Child jest renderowany po danym submodelu
|
|
if (Child->m_material) // o ile ma teksturę
|
|
if (Child->m_material != m_material) // i jest ona inna niż rodzica
|
|
Child->iFlags |= 0x80; // to trzeba sprawdzać, jak z teksturami jest
|
|
i = Child->FlagsCheck();
|
|
iFlags |= 0x00FF0000 & (i << 16 | i | i >> 8); // potomny, rodzeństwo i dzieci
|
|
if (eType == TP_TEXT)
|
|
{ // wyłączenie renderowania Next dla znaków
|
|
// wyświetlacza tekstowego
|
|
TSubModel *p = Child;
|
|
while (p)
|
|
{
|
|
p->iFlags &= 0xC0FFFFFF;
|
|
p = p->Next;
|
|
}
|
|
}
|
|
}
|
|
if (Next)
|
|
{ // Next jest renderowany po danym submodelu (kolejność odwrócona
|
|
// po wczytaniu T3D)
|
|
if (m_material) // o ile dany ma teksturę
|
|
if (m_material != Next->m_material || i & 0x00800000) // a ma inną albo dzieci zmieniają
|
|
iFlags |= 0x80; // to dany submodel musi sobie ją ustawiać
|
|
i = Next->FlagsCheck();
|
|
iFlags |= 0xFF000000 & (i << 24 | i << 8 | i); // następny, kolejne i ich dzieci
|
|
// tekstury nie ustawiamy tylko wtedy, gdy jest taka sama jak Next i jego
|
|
// dzieci nie zmieniają
|
|
}
|
|
return iFlags;
|
|
};
|
|
|
|
void TSubModel::SetRotate(float3 vNewRotateAxis, float fNewAngle)
|
|
{ // obrócenie submodelu wg podanej
|
|
// osi (np. wskazówki w kabinie)
|
|
v_RotateAxis = vNewRotateAxis;
|
|
f_Angle = fNewAngle;
|
|
if (fNewAngle != 0.0)
|
|
{
|
|
b_Anim = TAnimType::at_Rotate;
|
|
b_aAnim = TAnimType::at_Rotate;
|
|
}
|
|
iAnimOwner = iInstance; // zapamiętanie czyja jest animacja
|
|
}
|
|
|
|
void TSubModel::SetRotateXYZ(float3 vNewAngles)
|
|
{ // obrócenie submodelu o
|
|
// podane kąty wokół osi
|
|
// lokalnego układu
|
|
v_Angles = vNewAngles;
|
|
b_Anim = TAnimType::at_RotateXYZ;
|
|
b_aAnim = TAnimType::at_RotateXYZ;
|
|
iAnimOwner = iInstance; // zapamiętanie czyja jest animacja
|
|
}
|
|
|
|
void TSubModel::SetRotateXYZ(glm::vec3 vNewAngles)
|
|
{ // obrócenie submodelu o
|
|
// podane kąty wokół osi
|
|
// lokalnego układu
|
|
v_Angles.x = vNewAngles.x;
|
|
v_Angles.y = vNewAngles.y;
|
|
v_Angles.z = vNewAngles.z;
|
|
b_Anim = TAnimType::at_RotateXYZ;
|
|
b_aAnim = TAnimType::at_RotateXYZ;
|
|
iAnimOwner = iInstance; // zapamiętanie czyja jest animacja
|
|
}
|
|
|
|
void TSubModel::SetTranslate(float3 vNewTransVector)
|
|
{ // przesunięcie submodelu (np. w kabinie)
|
|
v_TransVector = vNewTransVector;
|
|
b_Anim = TAnimType::at_Translate;
|
|
b_aAnim = TAnimType::at_Translate;
|
|
iAnimOwner = iInstance; // zapamiętanie czyja jest animacja
|
|
}
|
|
|
|
void TSubModel::SetTranslate(glm::vec3 vNewTransVector)
|
|
{ // przesunięcie submodelu (np. w kabinie)
|
|
v_TransVector.x = vNewTransVector.x;
|
|
v_TransVector.y = vNewTransVector.y;
|
|
v_TransVector.z = vNewTransVector.z;
|
|
b_Anim = TAnimType::at_Translate;
|
|
b_aAnim = TAnimType::at_Translate;
|
|
iAnimOwner = iInstance; // zapamiętanie czyja jest animacja
|
|
}
|
|
|
|
void TSubModel::SetRotateIK1(float3 vNewAngles)
|
|
{ // obrócenie submodelu o
|
|
// podane kąty wokół osi
|
|
// lokalnego układu
|
|
v_Angles = vNewAngles;
|
|
iAnimOwner = iInstance; // zapamiętanie czyja jest animacja
|
|
}
|
|
|
|
struct ToLower
|
|
{
|
|
char operator()(char input)
|
|
{
|
|
return tolower(input);
|
|
}
|
|
};
|
|
|
|
TSubModel *TSubModel::GetFromName(std::string const &search, bool i)
|
|
{
|
|
TSubModel *result;
|
|
// std::transform(search.begin(),search.end(),search.begin(),ToLower());
|
|
// search=search.LowerCase();
|
|
// AnsiString name=AnsiString();
|
|
std::string search_lc = search;
|
|
if (i)
|
|
std::transform(search_lc.begin(), search_lc.end(), search_lc.begin(), ::tolower);
|
|
std::string pName_lc = pName;
|
|
if (i)
|
|
std::transform(pName_lc.begin(), pName_lc.end(), pName_lc.begin(), ::tolower);
|
|
if (pName.size() && search.size())
|
|
if (pName_lc == search_lc)
|
|
return this;
|
|
if (Next)
|
|
{
|
|
result = Next->GetFromName(search);
|
|
if (result)
|
|
return result;
|
|
}
|
|
if (Child)
|
|
{
|
|
result = Child->GetFromName(search);
|
|
if (result)
|
|
return result;
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
// WORD hbIndices[18]={3,0,1,5,4,2,1,0,4,1,5,3,2,3,5,2,4,0};
|
|
|
|
void TSubModel::RaAnimation(TAnimType a)
|
|
{
|
|
glm::mat4 m = OpenGLMatrices.data(GL_MODELVIEW);
|
|
RaAnimation(m, a);
|
|
glLoadMatrixf(glm::value_ptr(m));
|
|
}
|
|
|
|
void TSubModel::RaAnimation(glm::mat4 &m, TAnimType a)
|
|
{ // wykonanie animacji niezależnie od renderowania
|
|
switch (a)
|
|
{ // korekcja położenia, jeśli submodel jest animowany
|
|
case TAnimType::at_Translate: // Ra: było "true"
|
|
if (iAnimOwner != iInstance)
|
|
break; // cudza animacja
|
|
m = glm::translate(m, glm::vec3(v_TransVector.x, v_TransVector.y, v_TransVector.z));
|
|
break;
|
|
case TAnimType::at_Rotate: // Ra: było "true"
|
|
if (iAnimOwner != iInstance)
|
|
break; // cudza animacja
|
|
m = glm::rotate(m, glm::radians(f_Angle), glm::vec3(v_RotateAxis.x, v_RotateAxis.y, v_RotateAxis.z));
|
|
break;
|
|
case TAnimType::at_RotateXYZ:
|
|
if (iAnimOwner != iInstance)
|
|
break; // cudza animacja
|
|
m = glm::translate(m, glm::vec3(v_TransVector.x, v_TransVector.y, v_TransVector.z));
|
|
m = glm::rotate(m, glm::radians(v_Angles.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
|
m = glm::rotate(m, glm::radians(v_Angles.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
m = glm::rotate(m, glm::radians(v_Angles.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
|
break;
|
|
case TAnimType::at_SecondsJump: // sekundy z przeskokiem
|
|
m = glm::rotate(m, glm::radians(simulation::Time.data().wSecond * 6.0f), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_MinutesJump: // minuty z przeskokiem
|
|
m = glm::rotate(m, glm::radians(simulation::Time.data().wMinute * 6.0f), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_HoursJump: // godziny skokowo 12h/360°
|
|
m = glm::rotate(m, glm::radians(simulation::Time.data().wHour * 30.0f * 0.5f), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Hours24Jump: // godziny skokowo 24h/360°
|
|
m = glm::rotate(m, glm::radians(simulation::Time.data().wHour * 15.0f * 0.25f), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Seconds: // sekundy płynnie
|
|
m = glm::rotate(m, glm::radians((float)simulation::Time.second() * 6.0f), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Minutes: // minuty płynnie
|
|
m = glm::rotate(m, glm::radians(simulation::Time.data().wMinute * 6.0f + (float)simulation::Time.second() * 0.1f), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Hours: // godziny płynnie 12h/360°
|
|
// glRotatef(GlobalTime->hh*30.0+GlobalTime->mm*0.5+GlobalTime->mr/120.0,0.0,1.0,0.0);
|
|
m = glm::rotate(m, glm::radians(2.0f * (float)Global.fTimeAngleDeg), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Hours24: // godziny płynnie 24h/360°
|
|
// glRotatef(GlobalTime->hh*15.0+GlobalTime->mm*0.25+GlobalTime->mr/240.0,0.0,1.0,0.0);
|
|
m = glm::rotate(m, glm::radians((float)Global.fTimeAngleDeg), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Billboard: // obrót w pionie do kamery
|
|
{
|
|
glm::mat4 mat = glm::make_mat4(OpenGLMatrices.data_array(GL_MODELVIEW));
|
|
float3 gdzie = float3(mat[3][0], mat[3][1], mat[3][2]); // początek układu współrzędnych submodelu względem kamery
|
|
m = glm::mat4(1.0f);
|
|
m = glm::translate(m, glm::vec3(gdzie.x, gdzie.y, gdzie.z)); // początek układu zostaje bez zmian
|
|
m = glm::rotate(m, (float)atan2(gdzie.x, gdzie.z), glm::vec3(0.0f, 1.0f, 0.0f)); // jedynie obracamy w pionie o kąt
|
|
}
|
|
break;
|
|
case TAnimType::at_Wind: // ruch pod wpływem wiatru (wiatr będziemy liczyć potem...)
|
|
m = glm::rotate(m, glm::radians(1.5f * (float)sin(M_PI * simulation::Time.second() / 6.0)), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_Sky: // animacja nieba
|
|
m = glm::rotate(m, glm::radians((float)Global.fLatitudeDeg), glm::vec3(0.0f, 1.0f, 0.0f)); // ustawienie osi OY na północ
|
|
m = glm::rotate(m, glm::radians((float)-fmod(Global.fTimeAngleDeg, 360.0)), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
break;
|
|
case TAnimType::at_DigiClk: // animacja zegara cyfrowego
|
|
{ // ustawienie animacji w submodelach potomnych
|
|
TSubModel *sm = ChildGet();
|
|
do
|
|
{ // pętla po submodelach potomnych i obracanie ich o kąt zależy od czasu
|
|
if (sm->pName.size())
|
|
{
|
|
// musi mieć niepustą nazwę
|
|
if (sm->pName[0] >= '0' && sm->pName[0] <= '5')
|
|
{
|
|
// zegarek ma 6 cyfr maksymalnie
|
|
sm->SetRotate(float3(0, 1, 0), -Global.fClockAngleDeg[sm->pName[0] - '0']);
|
|
}
|
|
}
|
|
sm = sm->NextGet();
|
|
} while (sm);
|
|
}
|
|
break;
|
|
}
|
|
if (mAnimMatrix) // można by to dać np. do at_Translate
|
|
{
|
|
m *= glm::make_mat4(mAnimMatrix->e);
|
|
mAnimMatrix.reset(); // jak animator będzie potrzebował, to ustawi ponownie
|
|
}
|
|
};
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
void TSubModel::serialize_geometry(std::ostream &Output, bool const Packed, bool const Indexed, bool const UserData) const
|
|
{
|
|
|
|
if (Child)
|
|
{
|
|
Child->serialize_geometry(Output, Packed, Indexed, UserData);
|
|
}
|
|
if (m_geometry.handle != null_handle)
|
|
{
|
|
if (Packed)
|
|
{
|
|
auto vertices = GfxRenderer->Vertices(m_geometry.handle);
|
|
auto userdatas = GfxRenderer->UserData(m_geometry.handle);
|
|
bool has_userdata = !userdatas.empty();
|
|
for (int i = 0; i < vertices.size(); ++i)
|
|
{
|
|
vertices[i].serialize_packed(Output, Indexed);
|
|
if (UserData)
|
|
{
|
|
if (has_userdata)
|
|
userdatas[i].serialize_packed(Output);
|
|
else
|
|
gfx::vertex_userdata{}.serialize_packed(Output);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto vertices = GfxRenderer->Vertices(m_geometry.handle);
|
|
auto userdatas = GfxRenderer->UserData(m_geometry.handle);
|
|
bool has_userdata = !userdatas.empty();
|
|
for (int i = 0; i < vertices.size(); ++i)
|
|
{
|
|
vertices[i].serialize(Output, Indexed);
|
|
if (UserData)
|
|
{
|
|
if (has_userdata)
|
|
userdatas[i].serialize(Output);
|
|
else
|
|
gfx::vertex_userdata{}.serialize(Output);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Next)
|
|
{
|
|
Next->serialize_geometry(Output, Packed, Indexed, UserData);
|
|
}
|
|
};
|
|
|
|
int TSubModel::index_size() const
|
|
{
|
|
|
|
int size{1};
|
|
if (Child)
|
|
{
|
|
size = std::max(size, Child->index_size());
|
|
}
|
|
if (size < 4 && m_geometry.handle != null_handle)
|
|
{
|
|
auto const indexcount{GfxRenderer->Indices(m_geometry.handle).size()};
|
|
size = std::max(size, indexcount >= 1 << 16 ? 4 : indexcount >= 1 << 8 ? 2 : 1);
|
|
}
|
|
if (size < 4 && Next)
|
|
{
|
|
size = std::max(size, Next->index_size());
|
|
}
|
|
return size;
|
|
}
|
|
|
|
void TSubModel::serialize_indices(std::ostream &Output, int const Size) const
|
|
{
|
|
|
|
if (Child)
|
|
{
|
|
Child->serialize_indices(Output, Size);
|
|
}
|
|
if (m_geometry.handle != null_handle)
|
|
{
|
|
switch (Size)
|
|
{
|
|
case 1:
|
|
{
|
|
for (auto const &index : GfxRenderer->Indices(m_geometry.handle))
|
|
{
|
|
sn_utils::s_uint8(Output, index);
|
|
}
|
|
break;
|
|
}
|
|
case 2:
|
|
{
|
|
for (auto const &index : GfxRenderer->Indices(m_geometry.handle))
|
|
{
|
|
sn_utils::ls_uint16(Output, index);
|
|
}
|
|
break;
|
|
}
|
|
case 4:
|
|
{
|
|
for (auto const &index : GfxRenderer->Indices(m_geometry.handle))
|
|
{
|
|
sn_utils::ls_uint32(Output, index);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (Next)
|
|
{
|
|
Next->serialize_indices(Output, Size);
|
|
}
|
|
};
|
|
|
|
void TSubModel::create_geometry(std::size_t &Indexoffset, std::size_t &Vertexoffset, gfx::geometrybank_handle const &Bank)
|
|
{
|
|
|
|
// data offset is used to determine data offset of each submodel into single shared geometry bank
|
|
// (the offsets are part of legacy system which we now need to work around for backward compatibility)
|
|
if (Child)
|
|
{
|
|
Child->create_geometry(Indexoffset, Vertexoffset, Bank);
|
|
}
|
|
|
|
if (false == Vertices.empty())
|
|
{
|
|
|
|
m_geometry.index_offset = static_cast<int>(Indexoffset);
|
|
Indexoffset += Indices.size();
|
|
m_geometry.vertex_offset = static_cast<int>(Vertexoffset);
|
|
Vertexoffset += Vertices.size();
|
|
// conveniently all relevant custom node types use GL_POINTS, or we'd have to determine the type on individual basis
|
|
auto type = eType < TP_ROTATOR ? eType : GL_POINTS;
|
|
m_geometry.handle = GfxRenderer->Insert(Indices, Vertices, Userdata, Bank, type);
|
|
}
|
|
|
|
if (m_geometry.handle != 0)
|
|
{
|
|
// calculate bounding radius while we're at it
|
|
float squaredradius{};
|
|
// if this happens to be root node it may already have non-squared radius of the largest child
|
|
// since we're comparing squared radii, we need to square it back for correct results
|
|
m_boundingradius *= m_boundingradius;
|
|
auto const submodeloffset{offset(std::numeric_limits<float>::max())};
|
|
for (auto const &vertex : GfxRenderer->Vertices(m_geometry.handle))
|
|
{
|
|
squaredradius = glm::length2(submodeloffset + vertex.position);
|
|
if (squaredradius > m_boundingradius)
|
|
{
|
|
m_boundingradius = squaredradius;
|
|
}
|
|
}
|
|
if (m_boundingradius > 0.f)
|
|
{
|
|
m_boundingradius = std::sqrt(m_boundingradius);
|
|
}
|
|
// adjust overall radius if needed
|
|
// NOTE: the method to access root submodel is... less than ideal
|
|
auto *root{this};
|
|
while (root->Parent != nullptr)
|
|
{
|
|
root = root->Parent;
|
|
}
|
|
root->m_boundingradius = std::max(root->m_boundingradius, m_boundingradius);
|
|
}
|
|
|
|
if (Next)
|
|
{
|
|
Next->create_geometry(Indexoffset, Vertexoffset, Bank);
|
|
}
|
|
}
|
|
|
|
void TSubModel::ColorsSet(glm::vec3 const &Ambient, glm::vec3 const &Diffuse, glm::vec3 const &Specular)
|
|
{ // ustawienie kolorów dla modelu terenu
|
|
f4Ambient = glm::vec4(Ambient, 1.0f);
|
|
f4Diffuse = glm::vec4(Diffuse, 1.0f);
|
|
f4Specular = glm::vec4(Specular, 1.0f);
|
|
/*
|
|
int i;
|
|
if (a)
|
|
for (i = 0; i < 4; ++i)
|
|
f4Ambient[i] = a[i] / 255.0;
|
|
if (d)
|
|
for (i = 0; i < 4; ++i)
|
|
f4Diffuse[i] = d[i] / 255.0;
|
|
if (s)
|
|
for (i = 0; i < 4; ++i)
|
|
f4Specular[i] = s[i] / 255.0;
|
|
*/
|
|
};
|
|
|
|
bool TSubModel::is_emitter() const
|
|
{
|
|
|
|
return eType == TP_ROTATOR && ToLower(pName).find("smokesource_") == 0;
|
|
}
|
|
|
|
// pobranie transformacji względem wstawienia modelu
|
|
void TSubModel::ParentMatrix(float4x4 *m) const
|
|
{
|
|
|
|
m->Identity();
|
|
|
|
float4x4 submodelmatrix;
|
|
auto *submodel = this;
|
|
do
|
|
{
|
|
// for given step in hierarchy there can be custom transformation matrix, or no transformation
|
|
// retrieve it...
|
|
submodelmatrix.Identity();
|
|
if (submodel->GetMatrix())
|
|
{
|
|
submodelmatrix = float4x4(*submodel->GetMatrix());
|
|
}
|
|
// ...potentially adjust transformations of the root matrix if the model wasn't yet initialized...
|
|
if (submodel->Parent == nullptr && false == submodel->m_rotation_init_done)
|
|
{
|
|
// dla ostatniego może być potrzebny dodatkowy obrót, jeśli wczytano z T3D, a nie obrócono jeszcze
|
|
submodelmatrix.InitialRotate();
|
|
}
|
|
// ...combine the transformations...
|
|
*m = submodelmatrix * *m;
|
|
// ...and move up the transformation chain for the iteration...
|
|
submodel = submodel->Parent;
|
|
// ... until we hit the root
|
|
} while (submodel != nullptr);
|
|
/*
|
|
if( fMatrix != nullptr ) {
|
|
// skopiowanie, bo będziemy mnożyć
|
|
*m = float4x4( *fMatrix );
|
|
}
|
|
else {
|
|
m->Identity();
|
|
}
|
|
auto *sm = this;
|
|
while (sm->Parent)
|
|
{ // przenieść tę funkcję do modelu
|
|
if (sm->Parent->GetMatrix())
|
|
*m = *sm->Parent->GetMatrix() * *m;
|
|
sm = sm->Parent;
|
|
}
|
|
*/
|
|
};
|
|
|
|
void TSubModel::ReplaceMatrix(const glm::mat4 &mat)
|
|
{
|
|
*fMatrix = float4x4(glm::value_ptr(mat));
|
|
}
|
|
|
|
void TSubModel::ReplaceMaterial(const std::string &name)
|
|
{
|
|
m_material = GfxRenderer->Fetch_Material(name);
|
|
}
|
|
|
|
// obliczenie maksymalnej wysokości, na początek ślizgu w pantografie
|
|
float TSubModel::MaxY(float4x4 const &m)
|
|
{
|
|
// tylko dla trójkątów liczymy
|
|
if (eType != GL_TRIANGLES)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
auto maxy{0.0f};
|
|
// binary and text models invoke this function at different stages, either after or before geometry data was sent to the geometry manager
|
|
if (m_geometry.handle != null_handle)
|
|
{
|
|
|
|
for (auto const &vertex : GfxRenderer->Vertices(m_geometry.handle))
|
|
{
|
|
maxy = std::max(maxy, m[0][1] * vertex.position.x + m[1][1] * vertex.position.y + m[2][1] * vertex.position.z + m[3][1]);
|
|
}
|
|
}
|
|
else if (false == Vertices.empty())
|
|
{
|
|
|
|
for (auto const &vertex : Vertices)
|
|
{
|
|
maxy = std::max(maxy, m[0][1] * vertex.position.x + m[1][1] * vertex.position.y + m[2][1] * vertex.position.z + m[3][1]);
|
|
}
|
|
}
|
|
|
|
return maxy;
|
|
};
|
|
//---------------------------------------------------------------------------
|
|
|
|
TModel3d::~TModel3d()
|
|
{
|
|
|
|
if (iFlags & 0x0200)
|
|
{
|
|
// wczytany z pliku tekstowego, submodele sprzątają same
|
|
SafeDelete(Root);
|
|
}
|
|
else
|
|
{
|
|
// wczytano z pliku binarnego (jest właścicielem tablic)
|
|
SafeDeleteArray(Root); // submodele się usuną rekurencyjnie
|
|
}
|
|
};
|
|
|
|
TSubModel *TModel3d::AddToNamed(const char *Name, TSubModel *SubModel)
|
|
{
|
|
|
|
TSubModel *sm = Name ? GetFromName(Name) : nullptr;
|
|
if (sm == nullptr && Name != nullptr && std::strcmp(Name, "none") != 0)
|
|
{
|
|
ErrorLog("Bad model: parent for sub-model \"" + SubModel->pName + "\" doesn't exist or is located later in the model data", logtype::model);
|
|
}
|
|
AddTo(sm, SubModel); // szukanie nadrzędnego
|
|
return sm; // zwracamy wskaźnik do nadrzędnego submodelu
|
|
};
|
|
|
|
// jedyny poprawny sposób dodawania submodeli, inaczej mogą zginąć przy zapisie E3D
|
|
void TModel3d::AddTo(TSubModel *tmp, TSubModel *SubModel)
|
|
{
|
|
|
|
if (tmp)
|
|
{
|
|
// jeśli znaleziony, podłączamy mu jako potomny
|
|
tmp->ChildAdd(SubModel);
|
|
}
|
|
else
|
|
{ // jeśli nie znaleziony, podczepiamy do łańcucha głównego
|
|
SubModel->NextAdd(Root); // Ra: zmiana kolejności renderowania wymusza zmianę tu
|
|
Root = SubModel;
|
|
}
|
|
++iSubModelsCount; // teraz jest o 1 submodel więcej
|
|
iFlags |= 0x0200; // submodele są oddzielne
|
|
};
|
|
|
|
TSubModel *TModel3d::GetFromName(std::string const &Name) const
|
|
{ // wyszukanie submodelu po nazwie
|
|
if (Name.empty())
|
|
return Root; // potrzebne do terenu z E3D
|
|
if (iFlags & 0x0200) // wczytany z pliku tekstowego, wyszukiwanie rekurencyjne
|
|
return Root ? Root->GetFromName(Name) : nullptr;
|
|
else // wczytano z pliku binarnego, można wyszukać iteracyjnie
|
|
{
|
|
// for (int i=0;i<iSubModelsCount;++i)
|
|
return Root ? Root->GetFromName(Name) : nullptr;
|
|
}
|
|
};
|
|
|
|
// locates particle source submodels and stores them on internal list
|
|
nameoffset_sequence const &TModel3d::find_smoke_sources()
|
|
{
|
|
|
|
m_smokesources.clear();
|
|
if (Root != nullptr)
|
|
{
|
|
Root->find_smoke_sources(m_smokesources);
|
|
}
|
|
|
|
return smoke_sources();
|
|
}
|
|
|
|
// returns offset vector from root
|
|
glm::vec3 TSubModel::offset(float const Geometrytestoffsetthreshold) const
|
|
{
|
|
|
|
float4x4 parentmatrix;
|
|
ParentMatrix(&parentmatrix);
|
|
|
|
auto offset{glm::vec3{glm::make_mat4(parentmatrix.readArray()) * glm::vec4{0, 0, 0, 1}}};
|
|
|
|
if (glm::length(offset) < Geometrytestoffsetthreshold)
|
|
{
|
|
// offset of zero generally means the submodel has optimized identity matrix
|
|
// for such cases we resort to an estimate from submodel geometry
|
|
// TODO: do proper bounding area calculation for submodel when loading mesh and grab the centre point from it here
|
|
auto const &vertices{(m_geometry.handle != null_handle ? GfxRenderer->Vertices(m_geometry.handle) : Vertices)};
|
|
if (false == vertices.empty())
|
|
{
|
|
// transformation matrix for the submodel can still contain rotation and/or scaling,
|
|
// so we pass the vertex positions through it rather than just grab them directly
|
|
offset = glm::vec3();
|
|
auto const vertexfactor{1.f / vertices.size()};
|
|
auto const transformationmatrix{glm::make_mat4(parentmatrix.readArray())};
|
|
for (auto const &vertex : vertices)
|
|
{
|
|
offset += glm::vec3{transformationmatrix * glm::vec4{vertex.position, 1}} * vertexfactor;
|
|
}
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
bool TModel3d::LoadFromFile(std::string const &FileName, bool dynamic)
|
|
{
|
|
// wczytanie modelu z pliku
|
|
/*
|
|
// NOTE: disabled, this work is now done by the model manager
|
|
std::string name = ToLower(FileName);
|
|
// trim extension if needed
|
|
if( name.rfind( '.' ) != std::string::npos )
|
|
{
|
|
name.erase(name.rfind('.'));
|
|
}
|
|
*/
|
|
auto const name{FileName};
|
|
// cache the file name, in case someone wants it later
|
|
m_filename = name;
|
|
|
|
asBinary = name + ".e3d";
|
|
|
|
// Hirek: Jesli mamy ustawione priorityLoadText3D na yes to wpierw ladujemy t3d
|
|
if (Global.priorityLoadText3D && FileExists(name + ".t3d"))
|
|
{
|
|
WriteLog("Forced loading text model \"" + name + ".t3d\"");
|
|
LoadFromTextFile(name + ".t3d", dynamic);
|
|
if (!dynamic)
|
|
Init();
|
|
}
|
|
else if (FileExists(asBinary))
|
|
{
|
|
LoadFromBinFile(asBinary, dynamic);
|
|
asBinary = "";
|
|
Init();
|
|
}
|
|
else if (FileExists(name + ".t3d"))
|
|
{
|
|
LoadFromTextFile(name + ".t3d", dynamic);
|
|
if (!dynamic)
|
|
Init();
|
|
}
|
|
|
|
bool const result = Root ? iSubModelsCount > 0 : false; // brak pliku albo problem z wczytaniem
|
|
if (false == result)
|
|
{
|
|
ErrorLog("Bad model: failed to load 3d model \"" + name + "\"");
|
|
}
|
|
return result;
|
|
};
|
|
|
|
// E3D serialization
|
|
// http://rainsted.com/pl/Format_binarny_modeli_-_E3D
|
|
|
|
// m7todo: wymyślić lepszą nazwę
|
|
template <typename L, typename T> size_t get_container_pos(L &list, T o)
|
|
{
|
|
auto i = std::find(list.begin(), list.end(), o);
|
|
if (i == list.end())
|
|
{
|
|
list.push_back(o);
|
|
return list.size() - 1;
|
|
}
|
|
else
|
|
{
|
|
return std::distance(list.begin(), i);
|
|
}
|
|
}
|
|
|
|
// m7todo: za dużo argumentów, może przenieść do osobnej
|
|
// klasy serializera mającej własny stan, albo zrobić
|
|
// strukturę TModel3d::SerializerContext?
|
|
void TSubModel::serialize(std::ostream &s, std::vector<TSubModel *> &models, std::vector<std::string> &names, std::vector<std::string> &textures, std::vector<float4x4> &transforms)
|
|
{
|
|
size_t end = (size_t)s.tellp() + 256;
|
|
|
|
if (!Next)
|
|
sn_utils::ls_int32(s, -1);
|
|
else
|
|
sn_utils::ls_int32(s, (int32_t)get_container_pos(models, Next));
|
|
if (!Child)
|
|
sn_utils::ls_int32(s, -1);
|
|
else
|
|
sn_utils::ls_int32(s, (int32_t)get_container_pos(models, Child));
|
|
|
|
sn_utils::ls_int32(s, eType);
|
|
if (pName.size() == 0)
|
|
sn_utils::ls_int32(s, -1);
|
|
else
|
|
sn_utils::ls_int32(s, (int32_t)get_container_pos(names, pName));
|
|
sn_utils::ls_int32(s, (int)b_Anim);
|
|
|
|
uint32_t flags = iFlags;
|
|
if (m_material > 0 && Global.iConvertModels & 16)
|
|
flags &= ~0x30; // don't save phase information, will be guessed on binary load from material
|
|
sn_utils::ls_uint32(s, flags);
|
|
sn_utils::ls_int32(s, (int32_t)get_container_pos(transforms, *fMatrix));
|
|
|
|
sn_utils::ls_int32(s, m_geometry.vertex_count);
|
|
sn_utils::ls_int32(s, m_geometry.vertex_offset);
|
|
|
|
if (m_material <= 0)
|
|
sn_utils::ls_int32(s, m_material);
|
|
else
|
|
sn_utils::ls_int32(s, (int32_t)get_container_pos(textures, m_materialname));
|
|
|
|
sn_utils::ls_float32(s, 1.f); // fVisible
|
|
sn_utils::ls_float32(s, fLight);
|
|
|
|
sn_utils::s_vec4(s, f4Ambient);
|
|
sn_utils::s_vec4(s, f4Diffuse);
|
|
sn_utils::s_vec4(s, f4Specular);
|
|
sn_utils::s_vec4(s, f4Emision);
|
|
|
|
sn_utils::ls_float32(s, fWireSize);
|
|
sn_utils::ls_float32(s, fSquareMaxDist);
|
|
sn_utils::ls_float32(s, fSquareMinDist);
|
|
|
|
sn_utils::ls_float32(s, fNearAttenStart);
|
|
sn_utils::ls_float32(s, fNearAttenEnd);
|
|
sn_utils::ls_uint32(s, bUseNearAtten ? 1 : 0);
|
|
|
|
sn_utils::ls_int32(s, iFarAttenDecay);
|
|
sn_utils::ls_float32(s, fFarDecayRadius);
|
|
sn_utils::ls_float32(s, fCosFalloffAngle);
|
|
sn_utils::ls_float32(s, fCosHotspotAngle);
|
|
sn_utils::ls_float32(s, fCosViewAngle);
|
|
|
|
sn_utils::ls_int32(s, m_geometry.index_count);
|
|
sn_utils::ls_int32(s, m_geometry.index_offset);
|
|
|
|
// external data
|
|
sn_utils::ls_float32(s, diffuseMultiplier);
|
|
|
|
// fill empty space
|
|
size_t fill = end - s.tellp();
|
|
for (size_t i = 0; i < fill; i++)
|
|
s.put(0);
|
|
}
|
|
|
|
void TModel3d::SaveToBinFile(std::string const &FileName)
|
|
{
|
|
WriteLog("saving e3d model..");
|
|
|
|
// m7todo: można by zoptymalizować robiąc unordered_map
|
|
// na wyszukiwanie numerów już dodanych stringów i osobno
|
|
// vector na wskaźniki do stringów w kolejności numeracji
|
|
// tylko czy potrzeba?
|
|
std::vector<TSubModel *> models;
|
|
models.push_back(Root);
|
|
std::vector<std::string> names;
|
|
std::vector<std::string> textures;
|
|
textures.push_back("");
|
|
std::vector<float4x4> transforms;
|
|
|
|
std::ofstream s(FileName, std::ios::binary);
|
|
|
|
sn_utils::ls_uint32(s, MAKE_ID4('E', '3', 'D', '0'));
|
|
auto const e3d_spos = s.tellp();
|
|
sn_utils::ls_uint32(s, 0);
|
|
bool has_any_userdata = Root->HasAnyVertexUserData();
|
|
|
|
{
|
|
sn_utils::ls_uint32(s, MAKE_ID4('S', 'U', 'B', '0'));
|
|
auto const sub_spos = s.tellp();
|
|
sn_utils::ls_uint32(s, 0);
|
|
for (size_t i = 0; i < models.size(); i++)
|
|
models[i]->serialize(s, models, names, textures, transforms);
|
|
auto const pos = s.tellp();
|
|
s.seekp(sub_spos);
|
|
sn_utils::ls_uint32(s, (uint32_t)(4 + pos - sub_spos));
|
|
s.seekp(pos);
|
|
}
|
|
|
|
sn_utils::ls_uint32(s, MAKE_ID4('T', 'R', 'A', '0'));
|
|
sn_utils::ls_uint32(s, 8 + (uint32_t)transforms.size() * 64);
|
|
for (size_t i = 0; i < transforms.size(); i++)
|
|
transforms[i].serialize_float32(s);
|
|
|
|
auto const isindexed{m_indexcount > 0};
|
|
if (isindexed)
|
|
{
|
|
auto const indexsize{Root->index_size()};
|
|
sn_utils::ls_uint32(s, MAKE_ID4('I', 'D', 'X', '0' + indexsize));
|
|
sn_utils::ls_uint32(s, 8 + m_indexcount * indexsize);
|
|
Root->serialize_indices(s, indexsize);
|
|
|
|
if (!(Global.iConvertModels & 8))
|
|
{
|
|
int modeltype = 1;
|
|
int vertexsize = 20;
|
|
if (has_any_userdata)
|
|
{
|
|
modeltype |= 4;
|
|
vertexsize += 8;
|
|
}
|
|
sn_utils::ls_uint32(s, MAKE_ID4('V', 'N', 'T', '0' + modeltype));
|
|
sn_utils::ls_uint32(s, 8 + m_vertexcount * vertexsize);
|
|
Root->serialize_geometry(s, true, true, has_any_userdata);
|
|
}
|
|
else
|
|
{
|
|
int modeltype = 2;
|
|
int vertexsize = 48;
|
|
if (has_any_userdata)
|
|
{
|
|
modeltype |= 4;
|
|
vertexsize += 16;
|
|
}
|
|
sn_utils::ls_uint32(s, MAKE_ID4('V', 'N', 'T', '0' + modeltype));
|
|
sn_utils::ls_uint32(s, 8 + m_vertexcount * vertexsize);
|
|
Root->serialize_geometry(s, false, true, has_any_userdata);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int modeltype = 0;
|
|
int vertexsize = 32;
|
|
if (has_any_userdata)
|
|
{
|
|
modeltype |= 4;
|
|
vertexsize += 16;
|
|
}
|
|
sn_utils::ls_uint32(s, MAKE_ID4('V', 'N', 'T', '0' + modeltype));
|
|
sn_utils::ls_uint32(s, 8 + m_vertexcount * vertexsize);
|
|
Root->serialize_geometry(s, false, false, has_any_userdata);
|
|
}
|
|
|
|
if (textures.size())
|
|
{
|
|
sn_utils::ls_uint32(s, MAKE_ID4('T', 'E', 'X', '0'));
|
|
auto const tex_spos = s.tellp();
|
|
sn_utils::ls_uint32(s, 0);
|
|
for (size_t i = 0; i < textures.size(); i++)
|
|
sn_utils::s_str(s, textures[i]);
|
|
auto const pos = s.tellp();
|
|
s.seekp(tex_spos);
|
|
sn_utils::ls_uint32(s, (uint32_t)(4 + pos - tex_spos));
|
|
s.seekp(pos);
|
|
}
|
|
|
|
if (names.size())
|
|
{
|
|
sn_utils::ls_uint32(s, MAKE_ID4('N', 'A', 'M', '0'));
|
|
auto const nam_spos = s.tellp();
|
|
sn_utils::ls_uint32(s, 0);
|
|
for (size_t i = 0; i < names.size(); i++)
|
|
sn_utils::s_str(s, names[i]);
|
|
auto const pos = s.tellp();
|
|
s.seekp(nam_spos);
|
|
sn_utils::ls_uint32(s, (uint32_t)(4 + pos - nam_spos));
|
|
s.seekp(pos);
|
|
}
|
|
|
|
auto const end = s.tellp();
|
|
s.seekp(e3d_spos);
|
|
sn_utils::ls_uint32(s, (uint32_t)(4 + end - e3d_spos));
|
|
s.close();
|
|
|
|
WriteLog("..done.");
|
|
}
|
|
|
|
void TSubModel::deserialize(std::istream &s)
|
|
{
|
|
iNext = sn_utils::ld_int32(s);
|
|
iChild = sn_utils::ld_int32(s);
|
|
|
|
eType = sn_utils::ld_int32(s);
|
|
iName = sn_utils::ld_int32(s);
|
|
|
|
b_Anim = (TAnimType)sn_utils::ld_int32(s);
|
|
|
|
iFlags = sn_utils::ld_uint32(s);
|
|
iMatrix = sn_utils::ld_int32(s);
|
|
|
|
m_geometry.vertex_count = sn_utils::ld_int32(s);
|
|
m_geometry.vertex_offset = sn_utils::ld_int32(s);
|
|
iTexture = sn_utils::ld_int32(s);
|
|
|
|
auto discard = sn_utils::ld_float32(s); // fVisible
|
|
fLight = sn_utils::ld_float32(s);
|
|
|
|
f4Ambient = sn_utils::d_vec4(s);
|
|
f4Diffuse = sn_utils::d_vec4(s);
|
|
f4Specular = sn_utils::d_vec4(s);
|
|
f4Emision = sn_utils::d_vec4(s);
|
|
|
|
fWireSize = sn_utils::ld_float32(s);
|
|
fSquareMaxDist = sn_utils::ld_float32(s);
|
|
fSquareMinDist = sn_utils::ld_float32(s);
|
|
|
|
fNearAttenStart = sn_utils::ld_float32(s);
|
|
fNearAttenEnd = sn_utils::ld_float32(s);
|
|
bUseNearAtten = sn_utils::ld_uint32(s) != 0;
|
|
iFarAttenDecay = sn_utils::ld_int32(s);
|
|
fFarDecayRadius = sn_utils::ld_float32(s);
|
|
fCosFalloffAngle = sn_utils::ld_float32(s);
|
|
fCosHotspotAngle = sn_utils::ld_float32(s);
|
|
fCosViewAngle = sn_utils::ld_float32(s);
|
|
// HACK: the values will be 0 also when reading legacy chunk
|
|
m_geometry.index_count = sn_utils::ld_int32(s);
|
|
m_geometry.index_offset = sn_utils::ld_int32(s);
|
|
|
|
// extra data block
|
|
diffuseMultiplier = sn_utils::ld_float32(s);
|
|
// only multiply diffuse on experimental renderer
|
|
if (!Global.NvRenderer)
|
|
f4Diffuse /= diffuseMultiplier <= 0.0 ? 1.0 : diffuseMultiplier;
|
|
|
|
// necessary rotations were already done during t3d->e3d conversion
|
|
m_rotation_init_done = true;
|
|
}
|
|
|
|
void TModel3d::deserialize(std::istream &s, size_t size, bool dynamic)
|
|
{
|
|
Root = nullptr;
|
|
if (m_geometrybank == null_handle)
|
|
{
|
|
m_geometrybank = GfxRenderer->Create_Bank();
|
|
}
|
|
|
|
std::streampos end = s.tellg() + (std::streampos)size;
|
|
bool hastangents{false};
|
|
bool hasuserdata{false};
|
|
|
|
while (s.tellg() < end)
|
|
{
|
|
uint32_t type = sn_utils::ld_uint32(s);
|
|
uint32_t size = sn_utils::ld_uint32(s) - 8;
|
|
size_t pos = s.tellg();
|
|
std::streampos end = pos + (std::streampos)size;
|
|
|
|
if ((type & 0x00FFFFFF) == MAKE_ID4('S', 'U', 'B', 0)) // submodels
|
|
{
|
|
if (Root != nullptr)
|
|
throw std::runtime_error("e3d: duplicated SUB chunk");
|
|
|
|
size_t sm_size = 256 + 64 * (((type & 0xFF000000) >> 24) - '0');
|
|
size_t sm_cnt = size / sm_size;
|
|
iSubModelsCount = (int)sm_cnt;
|
|
Root = new TSubModel[sm_cnt];
|
|
for (size_t i = 0; i < sm_cnt; ++i)
|
|
{
|
|
s.seekg(pos + sm_size * i);
|
|
Root[i].deserialize(s);
|
|
}
|
|
}
|
|
else if ((type & 0x00FFFFFF) == MAKE_ID4('V', 'N', 'T', 0)) // geometry vertices
|
|
{
|
|
// we rely on the SUB chunk coming before the vertex data, and on the overall vertex count matching the size of data in the chunk.
|
|
// geometry associated with chunks isn't stored in the same order as the chunks themselves, so we need to sort that out first
|
|
if (Root == nullptr)
|
|
throw std::runtime_error("e3d: VNT chunk encountered before SUB chunk");
|
|
std::vector<std::pair<int, int>> submodeloffsets; // vertex data offset, submodel index
|
|
submodeloffsets.reserve(iSubModelsCount);
|
|
for (auto submodelindex = 0; submodelindex < iSubModelsCount; ++submodelindex)
|
|
{
|
|
auto const &submodelgeometry{Root[submodelindex].m_geometry};
|
|
if (submodelgeometry.vertex_count <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
submodeloffsets.emplace_back(submodelgeometry.vertex_offset, submodelindex);
|
|
}
|
|
std::sort(std::begin(submodeloffsets), std::end(submodeloffsets), [](std::pair<int, int> const &Left, std::pair<int, int> const &Right) { return Left.first < Right.first; });
|
|
// once sorted we can grab geometry as it comes, and assign it to the chunks it belongs to
|
|
size_t const vertextype{(((type & 0xFF000000) >> 24) - '0')};
|
|
hastangents = (vertextype & 3) > 0;
|
|
hasuserdata = vertextype & 4;
|
|
size_t vertex_size = 0;
|
|
switch (vertextype & 3)
|
|
{
|
|
case 0:
|
|
case 2:
|
|
{
|
|
vertex_size = 8;
|
|
if (hastangents)
|
|
vertex_size += 4;
|
|
if (hasuserdata)
|
|
vertex_size += 4;
|
|
break;
|
|
}
|
|
case 1:
|
|
{
|
|
vertex_size = 4;
|
|
if (hastangents)
|
|
vertex_size += 1;
|
|
if (hasuserdata)
|
|
vertex_size += 2;
|
|
break;
|
|
}
|
|
}
|
|
for (auto const &submodeloffset : submodeloffsets)
|
|
{
|
|
s.seekg(pos + submodeloffset.first * vertex_size * sizeof(float)); // Ensure that we start reading from the correct offset even if geometry is not encoded contiguously
|
|
auto &submodel{Root[submodeloffset.second]};
|
|
auto const &submodelgeometry{submodel.m_geometry};
|
|
submodel.Vertices.resize(submodelgeometry.vertex_count);
|
|
if (hasuserdata)
|
|
submodel.Userdata.resize(submodelgeometry.vertex_count);
|
|
m_vertexcount += submodelgeometry.vertex_count;
|
|
switch (vertextype & 3)
|
|
{
|
|
case 0:
|
|
{
|
|
// legacy vnt0 format
|
|
for (int i = 0; i < submodel.Vertices.size(); ++i)
|
|
{
|
|
submodel.Vertices[i].deserialize(s, hastangents);
|
|
if (hasuserdata)
|
|
submodel.Userdata[i].deserialize(s);
|
|
if (submodel.eType < TP_ROTATOR)
|
|
{
|
|
// normal vectors debug routine
|
|
if (false == submodel.m_normalizenormals && std::abs(glm::length2(submodel.Vertices[i].normal) - 1.0f) > 0.01f)
|
|
{
|
|
submodel.m_normalizenormals = TSubModel::normalize; // we don't know if uniform scaling would suffice
|
|
WriteLog("Bad model: non-unit normal vector(s) encountered during sub-model geometry deserialization", logtype::model);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 1:
|
|
{
|
|
// expanded chunk formats
|
|
for (int i = 0; i < submodel.Vertices.size(); ++i)
|
|
{
|
|
submodel.Vertices[i].deserialize_packed(s, hastangents);
|
|
if (hasuserdata)
|
|
submodel.Userdata[i].deserialize_packed(s);
|
|
}
|
|
break;
|
|
}
|
|
case 2:
|
|
{
|
|
// expanded chunk formats
|
|
for (int i = 0; i < submodel.Vertices.size(); ++i)
|
|
{
|
|
submodel.Vertices[i].deserialize(s, hastangents);
|
|
if (hasuserdata)
|
|
submodel.Userdata[i].deserialize(s);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
// TBD, TODO: throw error here?
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ((type & 0x00FFFFFF) == MAKE_ID4('I', 'D', 'X', 0)) // geometry indices
|
|
{
|
|
// handled similarly to vertex data chunk
|
|
// we rely on the SUB chunk coming before the vertex data, and on the overall vertex count matching the size of data in the chunk.
|
|
// geometry associated with chunks isn't stored in the same order as the chunks themselves, so we need to sort that out first
|
|
if (Root == nullptr)
|
|
throw std::runtime_error("e3d: IDX chunk encountered before SUB chunk");
|
|
std::vector<std::pair<int, int>> submodeloffsets; // index data offset, submodel index
|
|
submodeloffsets.reserve(iSubModelsCount);
|
|
for (auto submodelindex = 0; submodelindex < iSubModelsCount; ++submodelindex)
|
|
{
|
|
auto const &submodelgeometry{Root[submodelindex].m_geometry};
|
|
if (submodelgeometry.index_count <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
submodeloffsets.emplace_back(submodelgeometry.index_offset, submodelindex);
|
|
}
|
|
std::sort(std::begin(submodeloffsets), std::end(submodeloffsets), [](std::pair<int, int> const &Left, std::pair<int, int> const &Right) { return Left.first < Right.first; });
|
|
// once sorted we can grab indices in a continuous read, and assign them to the chunks they belong to
|
|
size_t const indexsize{(((type & 0xFF000000) >> 24) - '0')};
|
|
for (auto const &submodeloffset : submodeloffsets)
|
|
{
|
|
s.seekg(pos + submodeloffset.first * indexsize);
|
|
auto &submodel{Root[submodeloffset.second]};
|
|
auto const &submodelgeometry{submodel.m_geometry};
|
|
submodel.Indices.resize(submodelgeometry.index_count);
|
|
m_indexcount += submodelgeometry.index_count;
|
|
switch (indexsize)
|
|
{
|
|
case 1:
|
|
{
|
|
for (auto &index : submodel.Indices)
|
|
{
|
|
index = sn_utils::d_uint8(s);
|
|
}
|
|
break;
|
|
}
|
|
case 2:
|
|
{
|
|
for (auto &index : submodel.Indices)
|
|
{
|
|
index = sn_utils::ld_uint16(s);
|
|
}
|
|
break;
|
|
}
|
|
case 4:
|
|
{
|
|
for (auto &index : submodel.Indices)
|
|
{
|
|
index = sn_utils::ld_uint32(s);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (type == MAKE_ID4('T', 'R', 'A', '0'))
|
|
{
|
|
if (false == Matrices.empty())
|
|
throw std::runtime_error("e3d: duplicated TRA chunk");
|
|
size_t t_cnt = size / 64;
|
|
|
|
Matrices.resize(t_cnt);
|
|
for (size_t i = 0; i < t_cnt; ++i)
|
|
Matrices[i].deserialize_float32(s);
|
|
}
|
|
else if (type == MAKE_ID4('T', 'R', 'A', '1'))
|
|
{
|
|
if (false == Matrices.empty())
|
|
throw std::runtime_error("e3d: duplicated TRA chunk");
|
|
size_t t_cnt = size / 128;
|
|
|
|
Matrices.resize(t_cnt);
|
|
for (size_t i = 0; i < t_cnt; ++i)
|
|
Matrices[i].deserialize_float64(s);
|
|
}
|
|
else if (type == MAKE_ID4('T', 'E', 'X', '0'))
|
|
{
|
|
if (Textures.size())
|
|
throw std::runtime_error("e3d: duplicated TEX chunk");
|
|
while (s.tellg() < end)
|
|
{
|
|
std::string str = sn_utils::d_str(s);
|
|
std::replace(str.begin(), str.end(), '\\', '/');
|
|
Textures.push_back(str);
|
|
}
|
|
}
|
|
else if (type == MAKE_ID4('N', 'A', 'M', '0'))
|
|
{
|
|
if (Names.size())
|
|
throw std::runtime_error("e3d: duplicated NAM chunk");
|
|
while (s.tellg() < end)
|
|
Names.push_back(sn_utils::d_str(s));
|
|
}
|
|
|
|
s.seekg(end);
|
|
}
|
|
|
|
if (!Root)
|
|
throw std::runtime_error("e3d: no submodels");
|
|
|
|
for (size_t i = 0; (int)i < iSubModelsCount; ++i)
|
|
{
|
|
Root[i].BinInit(Root, Matrices.data(), &Textures, &Names, dynamic);
|
|
|
|
if (Root[i].ChildGet())
|
|
Root[i].ChildGet()->Parent = &Root[i];
|
|
if (Root[i].NextGet())
|
|
Root[i].NextGet()->Parent = Root[i].Parent;
|
|
|
|
// remap geometry type for custom type submodels
|
|
int type;
|
|
switch (Root[i].eType)
|
|
{
|
|
case TP_FREESPOTLIGHT:
|
|
case TP_STARS:
|
|
{
|
|
type = GL_POINTS;
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
type = Root[i].eType;
|
|
break;
|
|
}
|
|
}
|
|
if (false == hastangents)
|
|
{
|
|
gfx::calculate_tangents(Root[i].Vertices, Root[i].Indices, type);
|
|
}
|
|
Root[i].m_geometry.handle = GfxRenderer->Insert(Root[i].Indices, Root[i].Vertices, Root[i].Userdata, m_geometrybank, type);
|
|
}
|
|
}
|
|
|
|
void TSubModel::BinInit(TSubModel *s, float4x4 *m, std::vector<std::string> *t, std::vector<std::string> *n, bool dynamic)
|
|
{ // ustawienie wskaźników w submodelu
|
|
// m7todo: brzydko
|
|
iVisible = 1; // tymczasowo używane
|
|
Child = iChild > 0 ? s + iChild : nullptr; // zerowy nie może być potomnym
|
|
Next = iNext > 0 ? s + iNext : nullptr; // zerowy nie może być następnym
|
|
fMatrix = iMatrix >= 0 && m ? m + iMatrix : nullptr;
|
|
|
|
if (n->size() && iName >= 0)
|
|
{
|
|
pName = n->at(iName);
|
|
if (!pName.empty())
|
|
{ // jeśli dany submodel jest zgaszonym światłem, to
|
|
// domyślnie go ukrywamy
|
|
if (pName.starts_with("Light_On"))
|
|
{ // jeśli jest światłem numerowanym
|
|
iVisible = 0; // to domyślnie wyłączyć, żeby się nie nakładało z obiektem "Light_Off"
|
|
}
|
|
else if (dynamic)
|
|
{ // inaczej wyłączało smugę w latarniach
|
|
if (pName.ends_with("_on"))
|
|
{
|
|
// jeśli jest kontrolką w stanie zapalonym to domyślnie wyłączyć,
|
|
// żeby się nie nakładało z obiektem "_off"
|
|
iVisible = 0;
|
|
}
|
|
}
|
|
// hack: reset specular light value for shadow submodels
|
|
if (pName == "cien")
|
|
{
|
|
f4Specular = glm::vec4{0.0f, 0.0f, 0.0f, 1.0f};
|
|
}
|
|
}
|
|
}
|
|
else
|
|
pName = "";
|
|
if (iTexture > 0)
|
|
{ // obsługa stałej tekstury
|
|
auto const materialindex = static_cast<std::size_t>(iTexture);
|
|
if (materialindex < t->size())
|
|
{
|
|
m_materialname = t->at(materialindex);
|
|
/*
|
|
if( m_materialname.find_last_of( "/" ) == std::string::npos ) {
|
|
m_materialname = Global.asCurrentTexturePath + m_materialname;
|
|
}
|
|
*/
|
|
m_material = GfxRenderer->Fetch_Material(m_materialname);
|
|
// if we don't have phase flags set for some reason, try to fix it
|
|
if (!(iFlags & 0x30) && m_material != null_handle)
|
|
{
|
|
const IMaterial *mat = GfxRenderer->Material(m_material);
|
|
float opacity = mat->get_or_guess_opacity();
|
|
|
|
// set phase flag based on material opacity
|
|
if (opacity == 0.0f)
|
|
iFlags |= 0x20; // translucent
|
|
else
|
|
iFlags |= 0x10; // opaque
|
|
}
|
|
|
|
if (m_material != null_handle)
|
|
{
|
|
IMaterial const *mat = GfxRenderer->Material(m_material);
|
|
/*
|
|
// if material does have opacity set, replace submodel opacity with it
|
|
if (mat.opacity)
|
|
{
|
|
iFlags &= ~0x30;
|
|
if (*mat.opacity == 0.0f)
|
|
iFlags |= 0x20; // translucent
|
|
else
|
|
iFlags |= 0x10; // opaque
|
|
}
|
|
*/
|
|
// replace submodel selfillum with material one
|
|
if (mat->GetSelfillum())
|
|
{
|
|
fLight = mat->GetSelfillum().value();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ErrorLog("Bad model: reference to nonexistent texture index in sub-model" + (pName.empty() ? "" : " \"" + pName + "\""), logtype::model);
|
|
m_material = null_handle;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (iTexture == 0)
|
|
m_material = GfxRenderer->Fetch_Material("colored");
|
|
else
|
|
m_material = iTexture;
|
|
}
|
|
|
|
b_aAnim = b_Anim; // skopiowanie animacji do drugiego cyklu
|
|
|
|
if (eType == TP_STARS)
|
|
{
|
|
m_material = GfxRenderer->Fetch_Material("stars");
|
|
iFlags |= 0x10;
|
|
}
|
|
else if (eType == TP_FREESPOTLIGHT && iFlags & 0x10)
|
|
{
|
|
// we've added light glare which needs to be rendered during transparent phase,
|
|
// but models converted to e3d before addition won't have the render flag set correctly for this
|
|
// so as a workaround we're doing it here manually
|
|
iFlags |= 0x20;
|
|
}
|
|
// intercept and fix hotspot values if specified in degrees and not directly
|
|
if (fCosFalloffAngle > 1.0f)
|
|
{
|
|
fCosFalloffAngle = std::cos(glm::radians(0.5f * fCosFalloffAngle));
|
|
}
|
|
if (fCosHotspotAngle > 1.0f)
|
|
{
|
|
fCosHotspotAngle = std::cos(glm::radians(0.5f * fCosHotspotAngle));
|
|
}
|
|
// cap specular values for legacy models
|
|
f4Specular = glm::vec4{std::clamp(f4Specular.r, 0.0f, 1.0f), std::clamp(f4Specular.g, 0.0f, 1.0f), std::clamp(f4Specular.b, 0.0f, 1.0f), std::clamp(f4Specular.a, 0.0f, 1.0f)};
|
|
|
|
iFlags &= ~0x0200; // wczytano z pliku binarnego (nie jest właścicielem tablic)
|
|
|
|
if (fMatrix != nullptr)
|
|
{
|
|
auto const matrix = glm::make_mat4(fMatrix->readArray());
|
|
glm::vec3 const scale{glm::length(glm::vec3(glm::column(matrix, 0))), glm::length(glm::vec3(glm::column(matrix, 1))), glm::length(glm::vec3(glm::column(matrix, 2)))};
|
|
if (std::abs(scale.x - 1.0f) > 0.01 || std::abs(scale.y - 1.0f) > 0.01 || std::abs(scale.z - 1.0f) > 0.01)
|
|
{
|
|
ErrorLog("Bad model: transformation matrix for sub-model \"" + pName + "\" imposes geometry scaling (factors: " + to_string(scale) + ")", logtype::model);
|
|
m_normalizenormals = std::abs(scale.x - scale.y) < 0.01f && std::abs(scale.y - scale.z) < 0.01f ? rescale : normalize;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TSubModel::HasAnyVertexUserData() const
|
|
{
|
|
for (const TSubModel *sm = this; sm; sm = sm->Next)
|
|
{
|
|
if (m_geometry.handle)
|
|
{
|
|
if (!GfxRenderer->UserData(m_geometry.handle).empty())
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (!sm->Userdata.empty())
|
|
return true;
|
|
}
|
|
if (sm->Child && sm->Child->HasAnyVertexUserData())
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic)
|
|
{ // wczytanie modelu z pliku binarnego
|
|
WriteLog("Loading binary format 3d model data from \"" + FileName + "\"...", logtype::model);
|
|
|
|
std::ifstream file(FileName, std::ios::binary);
|
|
|
|
uint32_t type = sn_utils::ld_uint32(file);
|
|
uint32_t size = sn_utils::ld_uint32(file) - 8;
|
|
|
|
if (type == MAKE_ID4('E', '3', 'D', '0'))
|
|
{
|
|
deserialize(file, size, dynamic);
|
|
file.close();
|
|
|
|
WriteLog("Finished loading 3d model data from \"" + FileName + "\"", logtype::model);
|
|
}
|
|
else
|
|
{
|
|
// throw std::runtime_error("e3d: unknown main chunk");
|
|
ErrorLog("Bad model: unknown main chunk in file \"" + FileName + "\"", logtype::model);
|
|
file.close();
|
|
}
|
|
};
|
|
|
|
TSubModel *TModel3d::AppendChildFromGeometry(const std::string &name, const std::string &parent, const gfx::vertex_array &vertices, const gfx::index_array &indices)
|
|
{
|
|
iFlags |= 0x0200;
|
|
|
|
TSubModel *sm = new TSubModel();
|
|
sm->Parent = AddToNamed(parent.c_str(), sm);
|
|
sm->m_geometry.vertex_count = vertices.size();
|
|
sm->m_geometry.index_count = indices.size();
|
|
sm->eType = GL_TRIANGLES;
|
|
sm->pName = name;
|
|
sm->m_material = GfxRenderer->Fetch_Material("colored");
|
|
sm->fMatrix = new float4x4();
|
|
sm->fMatrix->Identity();
|
|
sm->iFlags |= 0x10;
|
|
sm->iFlags |= 0x8000;
|
|
sm->WillBeAnimated();
|
|
if (vertices.empty())
|
|
sm->iFlags &= ~0x3F;
|
|
sm->Vertices = vertices;
|
|
sm->Indices = indices;
|
|
m_vertexcount += vertices.size();
|
|
m_indexcount += indices.size();
|
|
|
|
if (!Root)
|
|
Root = sm;
|
|
|
|
return sm;
|
|
}
|
|
|
|
void TModel3d::LoadFromTextFile(std::string const &FileName, bool dynamic)
|
|
{ // wczytanie submodelu z pliku tekstowego
|
|
WriteLog("Loading text format 3d model data from \"" + FileName + "\"...", logtype::model);
|
|
iFlags |= 0x0200; // wczytano z pliku tekstowego (właścicielami tablic są submodle)
|
|
cParser parser(FileName, cParser::buffer_FILE); // Ra: tu powinno być "models/"...
|
|
TSubModel *SubModel;
|
|
std::string token = parser.getToken<std::string>();
|
|
while (token != "" || parser.eof())
|
|
{
|
|
std::string parent;
|
|
// parser.getToken(parent);
|
|
parser.getTokens(1, false); // nazwa submodelu nadrzędnego bez zmieny na małe
|
|
parser >> parent;
|
|
if (parent == "")
|
|
{
|
|
break;
|
|
}
|
|
SubModel = new TSubModel();
|
|
SubModel->Parent = GetFromName(parent);
|
|
|
|
{
|
|
auto const result{SubModel->Load(parser, dynamic)};
|
|
m_indexcount += result.first;
|
|
m_vertexcount += result.second;
|
|
}
|
|
|
|
if (SubModel->Parent == nullptr && parent != "none")
|
|
ErrorLog("Bad model: parent for sub-model \"" + SubModel->pName + "\" doesn't exist or is located later in the model data", logtype::model);
|
|
AddTo(SubModel->Parent, SubModel);
|
|
|
|
parser.getTokens();
|
|
parser >> token;
|
|
}
|
|
// Ra: od wersji 334 przechylany jest cały model, a nie tylko pierwszy submodel
|
|
// ale bujanie kabiny nadal używa bananów :( od 393 przywrócone, ale z dodatkowym warunkiem
|
|
if (Global.iConvertModels & 4)
|
|
{ // automatyczne banany czasem psuły przechylanie kabin...
|
|
if (dynamic && Root)
|
|
{
|
|
if (Root->NextGet()) // jeśli ma jakiekolwiek kolejne
|
|
{ // dynamic musi mieć "banana", bo tylko pierwszy obiekt jest animowany, a następne nie
|
|
SubModel = new TSubModel(); // utworzenie pustego
|
|
SubModel->ChildAdd(Root);
|
|
Root = SubModel;
|
|
++iSubModelsCount;
|
|
}
|
|
Root->WillBeAnimated(); // bo z tym jest dużo problemów
|
|
}
|
|
}
|
|
}
|
|
|
|
std::uint32_t TSubModel::VariantId(TSubModel const *Submodel)
|
|
{ // the seed is already well scattered, so the group is merely added to it. this way branches of one model
|
|
// are guaranteed to land on different variants, as long as the pool holds at least as many entries as
|
|
// the model has branches -- two containers carried by the same wagon won't come up with the same number
|
|
return iVariantSeed + static_cast<std::uint32_t>(Submodel != nullptr ? Submodel->m_variantgroup : 0);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// two branches sitting no farther than this from each other are treated as one object.
|
|
// generous, a simplified lod mesh doesn't have to sit exactly where the detailed one does
|
|
float const variantgrouptolerance{0.25f};
|
|
|
|
// assigns provided variant group id to the submodel and all of its children
|
|
void mark_variant_group(TSubModel *Submodel, int const Group, bool const Includesiblings)
|
|
{
|
|
while (Submodel != nullptr)
|
|
{
|
|
Submodel->m_variantgroup = Group;
|
|
mark_variant_group(Submodel->ChildGet(), Group, true);
|
|
if (false == Includesiblings)
|
|
{
|
|
break;
|
|
}
|
|
Submodel = Submodel->NextGet();
|
|
}
|
|
}
|
|
|
|
// expands provided bounding area to cover geometry carried by the submodel and all of its children
|
|
void accumulate_mesh_bounds(TSubModel const *Submodel, glm::vec3 &Minimum, glm::vec3 &Maximum, bool &Isempty, bool const Includesiblings)
|
|
{
|
|
while (Submodel != nullptr)
|
|
{
|
|
if (auto const &vertices{(Submodel->m_geometry.handle != null_handle ? GfxRenderer->Vertices(Submodel->m_geometry.handle) : Submodel->Vertices)};
|
|
false == vertices.empty())
|
|
{
|
|
// the transformation chain can still hold a rotation, so the vertices go through it
|
|
// rather than get taken at their face value
|
|
float4x4 parentmatrix;
|
|
Submodel->ParentMatrix(&parentmatrix);
|
|
auto const transformationmatrix{glm::make_mat4(parentmatrix.readArray())};
|
|
for (auto const &vertex : vertices)
|
|
{
|
|
auto const position{glm::vec3{transformationmatrix * glm::vec4{vertex.position, 1}}};
|
|
Minimum = glm::min(Minimum, position);
|
|
Maximum = glm::max(Maximum, position);
|
|
}
|
|
Isempty = false;
|
|
}
|
|
accumulate_mesh_bounds(Submodel->Child, Minimum, Maximum, Isempty, true);
|
|
if (false == Includesiblings)
|
|
{
|
|
break;
|
|
}
|
|
Submodel = Submodel->Next;
|
|
}
|
|
}
|
|
|
|
// returns position of provided branch of the model, relative to the model root
|
|
glm::vec3 branch_position(TSubModel const *Branch)
|
|
{
|
|
// the transformation chain is the cheap way to get there, as it doesn't touch the geometry,
|
|
// which keeps this affordable even for models made of thousands of branches...
|
|
auto const offset{Branch->offset()};
|
|
if (glm::length2(offset) > variantgrouptolerance * variantgrouptolerance)
|
|
{
|
|
return offset;
|
|
}
|
|
// ...but it only tells us anything for branches which actually carry a transform. models loaded from
|
|
// text format can have theirs baked into the vertices instead, which leaves the whole model piled up
|
|
// at the origin and, without the fallback below, all of its branches in a single variant group.
|
|
// NOTE: it's the centre of the bounding area we're after, and not the average vertex position offset()
|
|
// would give us -- the latter drifts with vertex density, which puts each lod mesh of one object in
|
|
// a group of its own and has the object change its texture variant as the lods get swapped
|
|
glm::vec3 minimum{std::numeric_limits<float>::max()};
|
|
glm::vec3 maximum{std::numeric_limits<float>::lowest()};
|
|
bool isempty{true};
|
|
accumulate_mesh_bounds(Branch, minimum, maximum, isempty, false);
|
|
return (false == isempty ? (minimum + maximum) * 0.5f : offset);
|
|
}
|
|
} // namespace
|
|
|
|
void TModel3d::assign_variant_groups()
|
|
{ // each top level branch of the model gets a group id, which it passes down to all of its children.
|
|
// for load models a branch is a single load chunk, and the group id is what makes two chunks of the same
|
|
// model pick different texture variants, while keeping all parts of one chunk on the same variant.
|
|
// the id comes from the position of the branch, not from its ordinal: lod variants of one object are
|
|
// separate branches drawn in disjoint distance ranges, and grouping them by position is what keeps the
|
|
// object on the same variant no matter which of them is currently on screen
|
|
std::vector<glm::vec3> grouppositions; // position of each group, indexed by its id
|
|
auto const groupid = [&grouppositions](TSubModel const *Branch) {
|
|
auto const position{branch_position(Branch)};
|
|
for (std::size_t index = 0; index < grouppositions.size(); ++index)
|
|
{
|
|
if (glm::length2(grouppositions[index] - position) < variantgrouptolerance * variantgrouptolerance)
|
|
{
|
|
return static_cast<int>(index);
|
|
}
|
|
}
|
|
grouppositions.emplace_back(position);
|
|
return static_cast<int>(grouppositions.size() - 1);
|
|
};
|
|
|
|
for (auto *trunk = Root; trunk != nullptr; trunk = trunk->NextGet())
|
|
{
|
|
// root level submodels are usually mere containers, but can carry geometry of their own
|
|
trunk->m_variantgroup = groupid(trunk);
|
|
for (auto *branch = trunk->ChildGet(); branch != nullptr; branch = branch->NextGet())
|
|
{
|
|
mark_variant_group(branch, groupid(branch), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TModel3d::Init()
|
|
{ // obrócenie początkowe układu współrzędnych, dla
|
|
// pojazdów wykonywane po analizie animacji
|
|
if (iFlags & 0x8000)
|
|
return; // operacje zostały już wykonane
|
|
if (Root)
|
|
{
|
|
if (iFlags & 0x0200)
|
|
{
|
|
// jeśli wczytano z pliku tekstowego jest jakiś dziwny błąd,
|
|
// że obkręcany ma być tylko ostatni submodel głównego łańcucha
|
|
// argumet określa, czy wykonać pierwotny obrót
|
|
Root->InitialRotate(true);
|
|
}
|
|
iFlags |= Root->FlagsCheck() | 0x8000; // flagi całego modelu
|
|
if (m_vertexcount)
|
|
{
|
|
if (m_geometrybank == null_handle)
|
|
{
|
|
m_geometrybank = GfxRenderer->Create_Bank();
|
|
}
|
|
std::size_t indexoffset = 0;
|
|
std::size_t vertexoffset = 0;
|
|
Root->create_geometry(indexoffset, vertexoffset, m_geometrybank);
|
|
}
|
|
// determine final bounding radius from the root-level siblings
|
|
auto const *root{Root};
|
|
while ((root = root->Next) != nullptr)
|
|
{
|
|
Root->m_boundingradius = std::max(Root->m_boundingradius, root->m_boundingradius);
|
|
}
|
|
|
|
assign_variant_groups();
|
|
|
|
if (Global.iConvertModels & 1 && false == asBinary.empty())
|
|
{
|
|
SaveToBinFile(asBinary);
|
|
asBinary = ""; // zablokowanie powtórnego zapisu
|
|
}
|
|
}
|
|
// check if the model contains particle emitters
|
|
find_smoke_sources();
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 2012-02 funkcje do tworzenia terenu z E3D
|
|
//-----------------------------------------------------------------------------
|
|
|
|
int TModel3d::TerrainCount() const
|
|
{ // zliczanie kwadratów kilometrowych (główna
|
|
// linia po Next) do tworznia tablicy
|
|
int i = 0;
|
|
TSubModel *r = Root;
|
|
while (r)
|
|
{
|
|
r = r->NextGet();
|
|
++i;
|
|
}
|
|
return i;
|
|
};
|
|
TSubModel *TModel3d::TerrainSquare(int n)
|
|
{ // pobieranie wskaźnika do submodelu (n)
|
|
int i = 0;
|
|
TSubModel *r = Root;
|
|
while (i < n)
|
|
{
|
|
r = r->NextGet();
|
|
++i;
|
|
}
|
|
r->UnFlagNext(); // blokowanie wyświetlania po Next głównej listy
|
|
return r;
|
|
};
|