Semi-working entity loading from XML files!
This commit is contained in:
@@ -132,9 +132,10 @@ private:
|
|||||||
void parseComponentInfo();
|
void parseComponentInfo();
|
||||||
void parseDefaults();
|
void parseDefaults();
|
||||||
void predictComponentAllocation();
|
void predictComponentAllocation();
|
||||||
void parseEntityGraph();
|
void parseEntityGraph(World* world, xercesc::DOMElement* parent, EntityID parentEntity);
|
||||||
std::size_t getTypeStride(std::string typeName);
|
std::size_t getTypeStride(std::string typeName);
|
||||||
float getFloatAttribute(const xercesc::DOMElement* element, const char* attribute) const;
|
float getFloatAttribute(const xercesc::DOMElement* element, const char* attribute) const;
|
||||||
|
void writeData(const xercesc::DOMElement* element, std::string typeName, char* outData);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -3,10 +3,12 @@
|
|||||||
<Entity xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xi="http://www.w3.org/2001/XInclude" xsi:noNamespaceSchemaLocation="../Types/Entity.xsd" xmlns:c="components">
|
<Entity xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xi="http://www.w3.org/2001/XInclude" xsi:noNamespaceSchemaLocation="../Types/Entity.xsd" xmlns:c="components">
|
||||||
<Components>
|
<Components>
|
||||||
<c:Transform>
|
<c:Transform>
|
||||||
<Position X="1.02" Y="3.123123" Z="41.123"/>
|
<Position X="0" Y="0" Z="0"/>
|
||||||
<Orientation I="1.234" J="1.234" K="1.234"/>
|
<Scale X="3" Y="3" Z="3"/>
|
||||||
<Scale X="1.02" Y="3.123123" Z="41.123"/>
|
|
||||||
</c:Transform>
|
</c:Transform>
|
||||||
|
<c:Model>
|
||||||
|
<Resource>Models/Core/UnitSphere.obj</Resource>
|
||||||
|
</c:Model>
|
||||||
<c:Test>
|
<c:Test>
|
||||||
<Integer>12</Integer>
|
<Integer>12</Integer>
|
||||||
<Double>11.11</Double>
|
<Double>11.11</Double>
|
||||||
@@ -14,7 +16,5 @@
|
|||||||
</c:Test>
|
</c:Test>
|
||||||
</Components>
|
</Components>
|
||||||
<Children>
|
<Children>
|
||||||
<xi:include href="OtherEntity.xml"/>
|
|
||||||
<xi:include href="OtherEntity.xml"/>
|
|
||||||
</Children>
|
</Children>
|
||||||
</Entity>
|
</Entity>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<xs:complexType>
|
<xs:complexType>
|
||||||
<xs:all>
|
<xs:all>
|
||||||
<xs:element ref="c:Transform" minOccurs="0"/>
|
<xs:element ref="c:Transform" minOccurs="0"/>
|
||||||
|
<xs:element ref="c:Model" minOccurs="0"/>
|
||||||
<xs:element ref="c:Test" minOccurs="0"/>
|
<xs:element ref="c:Test" minOccurs="0"/>
|
||||||
</xs:all>
|
</xs:all>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ void EntityXMLFile::PopulateWorld(World* world)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Parse entity hierarchy
|
// 4. Parse entity hierarchy
|
||||||
//parseEntityGraph();
|
auto root = m_DOMDocument->getDocumentElement();
|
||||||
|
parseEntityGraph(world, root, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -261,41 +262,7 @@ void EntityXMLFile::parseDefaults()
|
|||||||
|
|
||||||
std::string fieldType = ci.second.FieldTypes.at(fieldName);
|
std::string fieldType = ci.second.FieldTypes.at(fieldName);
|
||||||
unsigned int fieldOffset = ci.second.FieldOffsets.at(fieldName);
|
unsigned int fieldOffset = ci.second.FieldOffsets.at(fieldName);
|
||||||
|
writeData(fieldElement, fieldType, ci.second.Defaults.get() + fieldOffset);
|
||||||
XSValue::DataType dataType = XSValue::getDataType(XSTR(fieldType.c_str()));
|
|
||||||
if (dataType == XSValue::DataType::dt_MAXCOUNT) {
|
|
||||||
if (fieldType == "Vector") {
|
|
||||||
glm::vec3 vec;
|
|
||||||
vec.x = getFloatAttribute(fieldElement, "X");
|
|
||||||
vec.y = getFloatAttribute(fieldElement, "Y");
|
|
||||||
vec.z = getFloatAttribute(fieldElement, "Z");
|
|
||||||
memcpy(ci.second.Defaults.get() + fieldOffset, reinterpret_cast<char*>(&vec), getTypeStride(fieldType));
|
|
||||||
} else if (fieldType == "Color") {
|
|
||||||
glm::vec4 vec;
|
|
||||||
vec.r = getFloatAttribute(fieldElement, "R");
|
|
||||||
vec.g = getFloatAttribute(fieldElement, "G");
|
|
||||||
vec.b = getFloatAttribute(fieldElement, "B");
|
|
||||||
vec.a = getFloatAttribute(fieldElement, "A");
|
|
||||||
memcpy(ci.second.Defaults.get() + fieldOffset, reinterpret_cast<char*>(&vec), getTypeStride(fieldType));
|
|
||||||
} else if (fieldType == "Quaternion") {
|
|
||||||
glm::quat q;
|
|
||||||
q.x = getFloatAttribute(fieldElement, "X");
|
|
||||||
q.y = getFloatAttribute(fieldElement, "Y");
|
|
||||||
q.z = getFloatAttribute(fieldElement, "Z");
|
|
||||||
q.w = getFloatAttribute(fieldElement, "W");
|
|
||||||
memcpy(ci.second.Defaults.get() + fieldOffset, reinterpret_cast<char*>(&q), getTypeStride(fieldType));
|
|
||||||
}
|
|
||||||
} else if (dataType == XSValue::DataType::dt_string) {
|
|
||||||
char* str = XMLString::transcode(fieldElement->getTextContent());
|
|
||||||
std::string standardString(str);
|
|
||||||
//new (ci.second.Defaults.get() + fieldOffset) std::string(str);
|
|
||||||
XMLString::release(&str);
|
|
||||||
memcpy(ci.second.Defaults.get() + fieldOffset, &standardString, getTypeStride(fieldType));
|
|
||||||
} else {
|
|
||||||
XSValue::Status status;
|
|
||||||
XSValue* val = XSValue::getActualValue(fieldElement->getTextContent(), dataType, status);
|
|
||||||
memcpy(ci.second.Defaults.get() + fieldOffset, reinterpret_cast<char*>(&val->fData.fValue), getTypeStride(fieldType));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,11 +301,56 @@ void EntityXMLFile::predictComponentAllocation()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityXMLFile::parseEntityGraph()
|
void EntityXMLFile::parseEntityGraph(World* world, xercesc::DOMElement* element, EntityID parentEntity)
|
||||||
{
|
{
|
||||||
//using namespace xercesc;
|
using namespace xercesc;
|
||||||
|
|
||||||
//auto root = m_DOMDocument->getDocumentElement();
|
// Create entity
|
||||||
|
EntityID entity = world->CreateEntity(parentEntity);
|
||||||
|
LOG_DEBUG("Created entity %i, parent %i", entity, parentEntity);
|
||||||
|
|
||||||
|
// Add components
|
||||||
|
auto components = m_DOMDocument->evaluate(XSTR("Components/*"), element, nullptr, DOMXPathResult::ORDERED_NODE_SNAPSHOT_TYPE, nullptr);
|
||||||
|
for (int i = 0; i < components->getSnapshotLength(); i++) {
|
||||||
|
components->snapshotItem(i);
|
||||||
|
auto componentElement = dynamic_cast<DOMElement*>(components->getNodeValue());
|
||||||
|
std::string componentName = XSTR(componentElement->getLocalName());
|
||||||
|
auto& ci = m_ComponentInfo.at(componentName);
|
||||||
|
|
||||||
|
// Attach the component
|
||||||
|
auto c = world->AttachComponent(entity, componentName);
|
||||||
|
LOG_DEBUG("Attached %s component", componentName.c_str());
|
||||||
|
|
||||||
|
// Write field data
|
||||||
|
auto fields = componentElement->getChildNodes();
|
||||||
|
for (int j = 0; j < fields->getLength(); ++j) {
|
||||||
|
auto fieldNode = fields->item(j);
|
||||||
|
auto nodeType = fieldNode->getNodeType();
|
||||||
|
if (nodeType != DOMNode::ELEMENT_NODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto field = dynamic_cast<DOMElement*>(fields->item(j));
|
||||||
|
//const XMLCh* value = fields->item(j)->getTextContent();
|
||||||
|
std::string fieldName(XSTR(field->getLocalName()));
|
||||||
|
if (ci.FieldTypes.find(fieldName) == ci.FieldTypes.end()) {
|
||||||
|
std::cout << "Warning: Component \"" << componentName << "\" contains invalid field \"" << fieldName << "\". Skipping." << std::endl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fieldType = ci.FieldTypes.at(fieldName);
|
||||||
|
unsigned int fieldOffset = ci.FieldOffsets.at(fieldName);
|
||||||
|
std::string fieldValue(XSTR(field->getTextContent()));
|
||||||
|
LOG_DEBUG(" %s %s = %s", fieldType.c_str(), fieldName.c_str(), fieldValue.c_str());
|
||||||
|
writeData(field, fieldType, c.Data + fieldOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse children
|
||||||
|
auto children = m_DOMDocument->evaluate(XSTR("Children/Entity"), element, nullptr, DOMXPathResult::ORDERED_NODE_SNAPSHOT_TYPE, nullptr);
|
||||||
|
for (int i = 0; i < children->getSnapshotLength(); i++) {
|
||||||
|
children->snapshotItem(i);
|
||||||
|
parseEntityGraph(world, dynamic_cast<DOMElement*>(children->getNodeValue()), entity);
|
||||||
|
}
|
||||||
|
|
||||||
//auto components = m_DOMDocument->getElementsByTagNameNS(XSTR("components"), XSTR("*"));
|
//auto components = m_DOMDocument->getElementsByTagNameNS(XSTR("components"), XSTR("*"));
|
||||||
//for (int i = 0; i < components->getLength(); ++i) {
|
//for (int i = 0; i < components->getLength(); ++i) {
|
||||||
@@ -426,3 +438,44 @@ float EntityXMLFile::getFloatAttribute(const xercesc::DOMElement* element, const
|
|||||||
return val->fData.fValue.f_float;
|
return val->fData.fValue.f_float;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EntityXMLFile::writeData(const xercesc::DOMElement* element, std::string typeName, char* outData)
|
||||||
|
{
|
||||||
|
using namespace xercesc;
|
||||||
|
|
||||||
|
XSValue::DataType dataType = XSValue::getDataType(XSTR(typeName.c_str()));
|
||||||
|
if (dataType == XSValue::DataType::dt_MAXCOUNT) {
|
||||||
|
if (typeName == "Vector") {
|
||||||
|
glm::vec3 vec;
|
||||||
|
vec.x = getFloatAttribute(element, "X");
|
||||||
|
vec.y = getFloatAttribute(element, "Y");
|
||||||
|
vec.z = getFloatAttribute(element, "Z");
|
||||||
|
memcpy(outData, reinterpret_cast<char*>(&vec), getTypeStride(typeName));
|
||||||
|
} else if (typeName == "Color") {
|
||||||
|
glm::vec4 vec;
|
||||||
|
vec.r = getFloatAttribute(element, "R");
|
||||||
|
vec.g = getFloatAttribute(element, "G");
|
||||||
|
vec.b = getFloatAttribute(element, "B");
|
||||||
|
vec.a = getFloatAttribute(element, "A");
|
||||||
|
memcpy(outData, reinterpret_cast<char*>(&vec), getTypeStride(typeName));
|
||||||
|
} else if (typeName == "Quaternion") {
|
||||||
|
glm::quat q;
|
||||||
|
q.x = getFloatAttribute(element, "X");
|
||||||
|
q.y = getFloatAttribute(element, "Y");
|
||||||
|
q.z = getFloatAttribute(element, "Z");
|
||||||
|
q.w = getFloatAttribute(element, "W");
|
||||||
|
memcpy(outData, reinterpret_cast<char*>(&q), getTypeStride(typeName));
|
||||||
|
}
|
||||||
|
} else if (dataType == XSValue::DataType::dt_string) {
|
||||||
|
char* str = XMLString::transcode(element->getTextContent());
|
||||||
|
std::string standardString(str);
|
||||||
|
new (outData) std::string(str);
|
||||||
|
XMLString::release(&str);
|
||||||
|
//memcpy(outData, reinterpret_cast<char*>(&standardString), getTypeStride(typeName));
|
||||||
|
} else {
|
||||||
|
XSValue::Status status;
|
||||||
|
XSValue* val = XSValue::getActualValue(element->getTextContent(), dataType, status);
|
||||||
|
memcpy(outData, reinterpret_cast<char*>(&val->fData.fValue), getTypeStride(typeName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ RawModel::RawModel(std::string fileName)
|
|||||||
numIndices += face.mNumIndices;
|
numIndices += face.mNumIndices;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_DEBUG("Vertex count %i", numVertices);
|
//LOG_DEBUG("Vertex count %i", numVertices);
|
||||||
LOG_DEBUG("Index count %i", numIndices);
|
//LOG_DEBUG("Index count %i", numIndices);
|
||||||
|
|
||||||
LOG_DEBUG("Model has %i embedded textures", scene->mNumTextures);
|
//LOG_DEBUG("Model has %i embedded textures", scene->mNumTextures);
|
||||||
|
|
||||||
std::vector<std::tuple<std::string, glm::mat4>> boneInfo;
|
std::vector<std::tuple<std::string, glm::mat4>> boneInfo;
|
||||||
std::map<std::string, int> boneNameMapping;
|
std::map<std::string, int> boneNameMapping;
|
||||||
@@ -132,35 +132,35 @@ RawModel::RawModel(std::string fileName)
|
|||||||
matGroup.EndIndex = m_Indices.size() - 1;
|
matGroup.EndIndex = m_Indices.size() - 1;
|
||||||
// Material shininess
|
// Material shininess
|
||||||
material->Get(AI_MATKEY_SHININESS, matGroup.Shininess);
|
material->Get(AI_MATKEY_SHININESS, matGroup.Shininess);
|
||||||
LOG_DEBUG("Shininess: %f", matGroup.Shininess);
|
//LOG_DEBUG("Shininess: %f", matGroup.Shininess);
|
||||||
// Diffuse texture
|
// Diffuse texture
|
||||||
LOG_DEBUG("%i diffuse textures found", material->GetTextureCount(aiTextureType_DIFFUSE));
|
//LOG_DEBUG("%i diffuse textures found", material->GetTextureCount(aiTextureType_DIFFUSE));
|
||||||
if (material->GetTextureCount(aiTextureType_DIFFUSE)) {
|
if (material->GetTextureCount(aiTextureType_DIFFUSE)) {
|
||||||
aiString path;
|
aiString path;
|
||||||
aiTextureMapping mapping;
|
aiTextureMapping mapping;
|
||||||
material->GetTexture(aiTextureType_DIFFUSE, 0, &path, &mapping);
|
material->GetTexture(aiTextureType_DIFFUSE, 0, &path, &mapping);
|
||||||
std::string absolutePath = (boost::filesystem::path(fileName).branch_path() / path.C_Str()).string();
|
std::string absolutePath = (boost::filesystem::path(fileName).branch_path() / path.C_Str()).string();
|
||||||
LOG_DEBUG("Diffuse texture: %s", absolutePath.c_str());
|
//LOG_DEBUG("Diffuse texture: %s", absolutePath.c_str());
|
||||||
matGroup.Texture = std::shared_ptr<Texture>(ResourceManager::Load<Texture>(absolutePath));
|
matGroup.Texture = std::shared_ptr<Texture>(ResourceManager::Load<Texture>(absolutePath));
|
||||||
}
|
}
|
||||||
// Normal map
|
// Normal map
|
||||||
LOG_DEBUG("%i normal maps found", material->GetTextureCount(aiTextureType_HEIGHT));
|
//LOG_DEBUG("%i normal maps found", material->GetTextureCount(aiTextureType_HEIGHT));
|
||||||
if (material->GetTextureCount(aiTextureType_HEIGHT)) {
|
if (material->GetTextureCount(aiTextureType_HEIGHT)) {
|
||||||
aiString path;
|
aiString path;
|
||||||
aiTextureMapping mapping;
|
aiTextureMapping mapping;
|
||||||
material->GetTexture(aiTextureType_HEIGHT, 0, &path, &mapping);
|
material->GetTexture(aiTextureType_HEIGHT, 0, &path, &mapping);
|
||||||
std::string absolutePath = (boost::filesystem::path(fileName).branch_path() / path.C_Str()).string();
|
std::string absolutePath = (boost::filesystem::path(fileName).branch_path() / path.C_Str()).string();
|
||||||
LOG_DEBUG("Normal map: %s", absolutePath.c_str());
|
//LOG_DEBUG("Normal map: %s", absolutePath.c_str());
|
||||||
matGroup.NormalMap = std::shared_ptr<Texture>(ResourceManager::Load<Texture>(absolutePath));
|
matGroup.NormalMap = std::shared_ptr<Texture>(ResourceManager::Load<Texture>(absolutePath));
|
||||||
}
|
}
|
||||||
// Specular map
|
// Specular map
|
||||||
LOG_DEBUG("%i specular maps found", material->GetTextureCount(aiTextureType_SPECULAR));
|
//LOG_DEBUG("%i specular maps found", material->GetTextureCount(aiTextureType_SPECULAR));
|
||||||
if (material->GetTextureCount(aiTextureType_SPECULAR)) {
|
if (material->GetTextureCount(aiTextureType_SPECULAR)) {
|
||||||
aiString path;
|
aiString path;
|
||||||
aiTextureMapping mapping;
|
aiTextureMapping mapping;
|
||||||
material->GetTexture(aiTextureType_SPECULAR, 0, &path, &mapping);
|
material->GetTexture(aiTextureType_SPECULAR, 0, &path, &mapping);
|
||||||
std::string absolutePath = (boost::filesystem::path(fileName).branch_path() / path.C_Str()).string();
|
std::string absolutePath = (boost::filesystem::path(fileName).branch_path() / path.C_Str()).string();
|
||||||
LOG_DEBUG("Specular map: %s", absolutePath.c_str());
|
//LOG_DEBUG("Specular map: %s", absolutePath.c_str());
|
||||||
matGroup.SpecularMap = std::shared_ptr<Texture>(ResourceManager::Load<Texture>(absolutePath));
|
matGroup.SpecularMap = std::shared_ptr<Texture>(ResourceManager::Load<Texture>(absolutePath));
|
||||||
}
|
}
|
||||||
TextureGroups.push_back(matGroup);
|
TextureGroups.push_back(matGroup);
|
||||||
@@ -216,20 +216,20 @@ RawModel::RawModel(std::string fileName)
|
|||||||
m_Skeleton = new Skeleton();
|
m_Skeleton = new Skeleton();
|
||||||
CreateSkeleton(boneInfo, boneNameMapping, scene->mRootNode, -1);
|
CreateSkeleton(boneInfo, boneNameMapping, scene->mRootNode, -1);
|
||||||
int numBones = m_Skeleton->Bones.size();
|
int numBones = m_Skeleton->Bones.size();
|
||||||
LOG_DEBUG("Bone count: %i", numBones);
|
//LOG_DEBUG("Bone count: %i", numBones);
|
||||||
if (numBones > 0) {
|
if (numBones > 0) {
|
||||||
m_Skeleton->PrintSkeleton();
|
m_Skeleton->PrintSkeleton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animations
|
// Animations
|
||||||
LOG_DEBUG("Animation count: %i", scene->mNumAnimations);
|
//LOG_DEBUG("Animation count: %i", scene->mNumAnimations);
|
||||||
for (int i = 0; i < scene->mNumAnimations; ++i) {
|
for (int i = 0; i < scene->mNumAnimations; ++i) {
|
||||||
auto animation = scene->mAnimations[i];
|
auto animation = scene->mAnimations[i];
|
||||||
std::string animationName = animation->mName.C_Str();
|
std::string animationName = animation->mName.C_Str();
|
||||||
LOG_DEBUG("Animation: %s", animationName.c_str());
|
//LOG_DEBUG("Animation: %s", animationName.c_str());
|
||||||
LOG_DEBUG("Duration: %f", animation->mDuration);
|
//LOG_DEBUG("Duration: %f", animation->mDuration);
|
||||||
LOG_DEBUG("Ticks per second: %f", animation->mTicksPerSecond);
|
//LOG_DEBUG("Ticks per second: %f", animation->mTicksPerSecond);
|
||||||
|
|
||||||
Skeleton::Animation skelAnim;
|
Skeleton::Animation skelAnim;
|
||||||
skelAnim.Name = animationName;
|
skelAnim.Name = animationName;
|
||||||
@@ -303,7 +303,7 @@ void RawModel::CreateSkeleton(std::vector<std::tuple<std::string, glm::mat4>> &b
|
|||||||
|
|
||||||
// Find the bone by name in the bone info list
|
// Find the bone by name in the bone info list
|
||||||
if (boneNameMapping.find(nodeName) == boneNameMapping.end()) {
|
if (boneNameMapping.find(nodeName) == boneNameMapping.end()) {
|
||||||
LOG_DEBUG("Node \"%s\" was not a bone", nodeName.c_str());
|
//LOG_DEBUG("Node \"%s\" was not a bone", nodeName.c_str());
|
||||||
} else {
|
} else {
|
||||||
glm::mat4 offsetMatrix;
|
glm::mat4 offsetMatrix;
|
||||||
int ID = boneNameMapping[nodeName];
|
int ID = boneNameMapping[nodeName];
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ glm::vec3 GetAbsolutePosition(World* world, ComponentWrapper transformComponent)
|
|||||||
void RenderQueueFactory::FillModels(World* world, RenderQueue* renderQueue)
|
void RenderQueueFactory::FillModels(World* world, RenderQueue* renderQueue)
|
||||||
{
|
{
|
||||||
for(auto& modelC : world->GetComponents("Model")) {
|
for(auto& modelC : world->GetComponents("Model")) {
|
||||||
ModelJob job;
|
|
||||||
std::string resource = modelC["Resource"];
|
std::string resource = modelC["Resource"];
|
||||||
|
if (resource.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
glm::vec4 color = modelC["Color"];
|
glm::vec4 color = modelC["Color"];
|
||||||
Model* model = ResourceManager::Load<Model>(resource);
|
Model* model = ResourceManager::Load<Model>(resource);
|
||||||
|
|
||||||
for (auto texGroup : model->TextureGroups) {
|
for (auto texGroup : model->TextureGroups) {
|
||||||
|
ModelJob job;
|
||||||
job.TextureID = (texGroup.Texture) ? texGroup.Texture->ResourceID : 0;
|
job.TextureID = (texGroup.Texture) ? texGroup.Texture->ResourceID : 0;
|
||||||
job.DiffuseTexture = texGroup.Texture.get();
|
job.DiffuseTexture = texGroup.Texture.get();
|
||||||
job.NormalTexture = texGroup.NormalMap.get();
|
job.NormalTexture = texGroup.NormalMap.get();
|
||||||
|
|||||||
Reference in New Issue
Block a user