diff --git a/include/gfx.h b/include/gfx.h index 53e169e..8e0cc11 100644 --- a/include/gfx.h +++ b/include/gfx.h @@ -128,10 +128,10 @@ struct DescriptorSetLayoutBinding { }; struct SamplerInfo { - Filter minify; - Filter magnify; - Filter mipmap; - bool anisotropic_filtering; + Filter minify = gfx::Filter::kLinear; + Filter magnify = gfx::Filter::kLinear; + Filter mipmap = gfx::Filter::kLinear; + bool anisotropic_filtering = true; bool operator==(const SamplerInfo&) const = default; }; diff --git a/include/resources/texture.h b/include/resources/texture.h index 80c489f..093ea36 100644 --- a/include/resources/texture.h +++ b/include/resources/texture.h @@ -10,14 +10,8 @@ namespace engine { class Texture { public: - enum class Filtering { - kOff, - kBilinear, - kTrilinear, - kAnisotropic, - }; - - Texture(Renderer* renderer, const uint8_t* bitmap, int width, int height, Filtering filtering, bool srgb); + + Texture(Renderer* renderer, const uint8_t* bitmap, int width, int height, gfx::SamplerInfo samplerInfo, bool srgb); ~Texture(); Texture(const Texture&) = delete; @@ -32,7 +26,7 @@ class Texture { const gfx::Sampler* sampler_; // not owned by Texture, owned by Renderer }; -std::unique_ptr LoadTextureFromFile(const std::string& path, Texture::Filtering filtering, Renderer* renderer, bool srgb = true); +std::unique_ptr LoadTextureFromFile(const std::string& path, gfx::SamplerInfo samplerInfo, Renderer* renderer, bool srgb = true); } // namespace engine diff --git a/include/util/gltf_loader.h b/include/util/gltf_loader.h index 899967f..4a94e4f 100644 --- a/include/util/gltf_loader.h +++ b/include/util/gltf_loader.h @@ -6,6 +6,15 @@ namespace engine::util { +/* + * Loads the default scene found in a glTF file into 'scene'. + * 'isStatic' will mark every transform as static to aid rendering optimisation. + * Returns the top-level glTF node as an engine entity. + * + * Loader limitations: + * - Can only load .glb files + * - glTF files must contain all textures + */ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic = false); } \ No newline at end of file diff --git a/src/application.cpp b/src/application.cpp index 9c8b670..93598a9 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -167,13 +167,21 @@ Application::Application(const char* appName, const char* appVersion, gfx::Graph /* default textures */ { - auto whiteTexture = LoadTextureFromFile(GetResourcePath("engine/textures/white.png"), Texture::Filtering::kOff, renderer(), true); + auto whiteTexture = LoadTextureFromFile(GetResourcePath("engine/textures/white.png"), gfx::SamplerInfo{}, renderer(), true); GetResourceManager()->AddPersistent("builtin.white", std::move(whiteTexture)); } { - auto normalTexture = LoadTextureFromFile(GetResourcePath("engine/textures/normal.png"), Texture::Filtering::kOff, renderer(), false); + auto normalTexture = LoadTextureFromFile(GetResourcePath("engine/textures/normal.png"), gfx::SamplerInfo{}, renderer(), false); GetResourceManager()->AddPersistent("builtin.normal", std::move(normalTexture)); } + + /* default materials */ + { + auto defaultMaterial = std::make_unique(renderer(), GetResource("builtin.fancy")); + defaultMaterial->SetAlbedoTexture(GetResource("builtin.white")); + defaultMaterial->SetNormalTexture(GetResource("builtin.normal")); + GetResourceManager()->AddPersistent("builtin.default", std::move(defaultMaterial)); + } } Application::~Application() diff --git a/src/resources/texture.cpp b/src/resources/texture.cpp index 1acf5d2..2fdffbc 100644 --- a/src/resources/texture.cpp +++ b/src/resources/texture.cpp @@ -8,42 +8,13 @@ namespace engine { -Texture::Texture(Renderer* renderer, const uint8_t* bitmap, int width, int height, Filtering filtering, bool srgb) : gfx_(renderer->GetDevice()) +Texture::Texture(Renderer* renderer, const uint8_t* bitmap, int width, int height, gfx::SamplerInfo samplerInfo, bool srgb) : gfx_(renderer->GetDevice()) { - gfx::SamplerInfo samplerInfo{}; - - samplerInfo.magnify = gfx::Filter::kLinear; - - switch (filtering) { - case Filtering::kOff: - samplerInfo.minify = gfx::Filter::kNearest; - samplerInfo.mipmap = gfx::Filter::kNearest; - samplerInfo.anisotropic_filtering = false; - break; - case Filtering::kBilinear: - samplerInfo.minify = gfx::Filter::kLinear; - samplerInfo.mipmap = gfx::Filter::kNearest; - samplerInfo.anisotropic_filtering = false; - break; - case Filtering::kTrilinear: - samplerInfo.minify = gfx::Filter::kLinear; - samplerInfo.mipmap = gfx::Filter::kLinear; - samplerInfo.anisotropic_filtering = false; - break; - case Filtering::kAnisotropic: - samplerInfo.minify = gfx::Filter::kLinear; - samplerInfo.mipmap = gfx::Filter::kLinear; - samplerInfo.anisotropic_filtering = true; - } - if (renderer->samplers.contains(samplerInfo) == false) { renderer->samplers.insert(std::make_pair(samplerInfo, gfx_->CreateSampler(samplerInfo))); } - gfx::ImageFormat format = gfx::ImageFormat::kLinear; - if (srgb) { - format = gfx::ImageFormat::kSRGB; - } + gfx::ImageFormat format = srgb ? gfx::ImageFormat::kSRGB : gfx::ImageFormat::kLinear; image_ = gfx_->CreateImage(width, height, format, bitmap); sampler_ = renderer->samplers.at(samplerInfo); @@ -57,11 +28,11 @@ Texture::~Texture() gfx_->DestroyImage(image_); } -std::unique_ptr LoadTextureFromFile(const std::string& path, Texture::Filtering filtering, Renderer* renderer, bool srgb) +std::unique_ptr LoadTextureFromFile(const std::string& path, gfx::SamplerInfo samplerInfo, Renderer* renderer, bool srgb) { int width, height; auto bitmap = util::ReadImageFile(path, width, height); - return std::make_unique(renderer, bitmap->data(), width, height, filtering, srgb); + return std::make_unique(renderer, bitmap->data(), width, height, samplerInfo, srgb); } } // namespace engine \ No newline at end of file diff --git a/src/util/gltf_loader.cpp b/src/util/gltf_loader.cpp index 5a78a84..145e2d2 100644 --- a/src/util/gltf_loader.cpp +++ b/src/util/gltf_loader.cpp @@ -53,250 +53,6 @@ static glm::mat4 MatFromDoubleArray(const std::vector& arr) return mat; } -static void CreateNodes(engine::Scene& app_scene, const tg::Scene& gl_scene, const tg::Model& gl_model, engine::Entity parent_entity, const tg::Node& node) -{ - static int node_count = 0; - int node_uuid = node_count++; - - glm::vec3 pos{0.0f, 0.0f, 0.0f}; - glm::quat rot{0.0f, 0.0f, 0.0f, 1.0f}; - glm::vec3 scale{1.0f, 1.0f, 1.0f}; - - if (node.matrix.size() == 16) { - const glm::mat4 matrix = MatFromDoubleArray(node.matrix); - DecomposeTransform(matrix, pos, rot, scale); - } - else { - if (node.translation.size() == 3) { - pos.x = node.translation[0]; - pos.y = node.translation[1]; - pos.z = node.translation[2]; - } - if (node.rotation.size() == 4) { - rot.x = node.rotation[0]; - rot.y = node.rotation[1]; - rot.z = node.rotation[2]; - rot.w = node.rotation[3]; - } - if (node.scale.size() == 3) { - scale.x = node.scale[0]; - scale.y = node.scale[1]; - scale.z = node.scale[2]; - } - } - - engine::Entity entity = app_scene.CreateEntity(std::string("test_node") + std::to_string(node_uuid), parent_entity, pos, rot, scale); - - if (node.mesh >= 0) { - const tg::Mesh& mesh = gl_model.meshes.at(node.mesh); - const tg::Primitive& prim = mesh.primitives.front(); // required - if (prim.mode != TINYGLTF_MODE_TRIANGLES) throw std::runtime_error("glTF loader only supports triangles!"); - - const tg::Accessor& indices_accessor = gl_model.accessors.at(prim.indices); // not required. TODO: handle no indices - size_t indices_int_size = 0; - if (indices_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) indices_int_size = 1; - if (indices_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) indices_int_size = 2; - if (indices_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) indices_int_size = 4; - if (indices_int_size == 0) throw std::runtime_error("GLTF parse error!"); - const tg::BufferView& indices_bufferview = gl_model.bufferViews.at(indices_accessor.bufferView); // required for TG - const size_t indices_byteoffset = indices_accessor.byteOffset + indices_bufferview.byteOffset; - const tg::Buffer& indices_buffer = gl_model.buffers.at(indices_bufferview.buffer); - - std::unique_ptr> indices = nullptr; - if (indices_int_size == 4) { - const uint32_t* const indices_data = reinterpret_cast(indices_buffer.data.data() + indices_byteoffset); - // in future, let Mesh constructor use spans to avoid unneccesary copy here - indices = std::make_unique>(indices_data, indices_data + indices_accessor.count); - } - else if (indices_int_size == 2) { - indices = std::make_unique>(); - const uint16_t* const indices_data = reinterpret_cast(indices_buffer.data.data() + indices_byteoffset); - for (size_t i = 0; i < indices_accessor.count; ++i) { - indices->push_back(indices_data[i]); - } - } - - if (indices == nullptr) { - throw std::runtime_error("TODO: handle and support this"); - } - - const tg::Accessor& pos_accessor = gl_model.accessors.at(prim.attributes.at("POSITION")); - if (pos_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) throw std::runtime_error("Position att. must be float!"); - if (pos_accessor.type != 3) throw std::runtime_error("Position att. dim. must be 3!"); - const tg::BufferView& pos_bufferview = gl_model.bufferViews.at(pos_accessor.bufferView); - const size_t pos_byteoffset = pos_accessor.byteOffset + pos_bufferview.byteOffset; - const size_t pos_bytestride = pos_accessor.ByteStride(pos_bufferview); - const tg::Buffer& pos_buffer = gl_model.buffers.at(pos_bufferview.buffer); - - const tg::Accessor& norm_accessor = gl_model.accessors.at(prim.attributes.at("NORMAL")); - if (norm_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) throw std::runtime_error("Normal att. must be float!"); - if (norm_accessor.type != 3) throw std::runtime_error("Normal att. dim. must be 3!"); - const tg::BufferView& norm_bufferview = gl_model.bufferViews.at(norm_accessor.bufferView); - const size_t norm_byteoffset = norm_accessor.byteOffset + norm_bufferview.byteOffset; - const size_t norm_bytestride = norm_accessor.ByteStride(norm_bufferview); - const tg::Buffer& norm_buffer = gl_model.buffers.at(norm_bufferview.buffer); - - std::vector vertices(pos_accessor.count); - - if (prim.attributes.contains("TEXCOORD_0")) { - const tg::Accessor& uv_accessor = gl_model.accessors.at(prim.attributes.at("TEXCOORD_0")); - if (uv_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) throw std::runtime_error("UV att. must be float!"); - if (uv_accessor.type != 2) throw std::runtime_error("UV att. dim. must be 2!"); - const tg::BufferView& uv_bufferview = gl_model.bufferViews.at(uv_accessor.bufferView); - const size_t uv_byteoffset = uv_accessor.byteOffset + uv_bufferview.byteOffset; - const size_t uv_bytestride = uv_accessor.ByteStride(uv_bufferview); - const tg::Buffer& uv_buffer = gl_model.buffers.at(uv_bufferview.buffer); - - for (size_t i = 0; i < vertices.size(); ++i) { - vertices[i].uv = *reinterpret_cast(&uv_buffer.data[uv_byteoffset + uv_bytestride * i]); - } - } - - bool has_tangents = false; - if (prim.attributes.contains("TANGENT")) { - has_tangents = true; - } - - // copy everything except tangents and uvs - for (size_t i = 0; i < vertices.size(); ++i) { - vertices[i].pos = *reinterpret_cast(&pos_buffer.data[pos_byteoffset + pos_bytestride * i]); - vertices[i].norm = *reinterpret_cast(&norm_buffer.data[norm_byteoffset + norm_bytestride * i]); - } - - if (has_tangents) { - const tg::Accessor& tangent_accessor = gl_model.accessors.at(prim.attributes.at("TANGENT")); - if (tangent_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) throw std::runtime_error("Tangent att. must be float!"); - if (tangent_accessor.type != 4) throw std::runtime_error("Tangent att. dim. must be 4!"); - const tg::BufferView& tangent_bufferview = gl_model.bufferViews.at(tangent_accessor.bufferView); - const size_t tangent_byteoffset = tangent_accessor.byteOffset + tangent_bufferview.byteOffset; - const size_t tangent_bytestride = tangent_accessor.ByteStride(tangent_bufferview); - const tg::Buffer& tangent_buffer = gl_model.buffers.at(tangent_bufferview.buffer); - for (size_t i = 0; i < vertices.size(); ++i) { - vertices[i].tangent = *reinterpret_cast(&tangent_buffer.data[tangent_byteoffset + tangent_bytestride * i]); - } - } - else { - // generate tangents if they're not in the file - struct MeshData { - engine::Vertex* vertices; - const uint32_t* indices; - size_t count; - std::vector new_indices; - }; - - MeshData meshData{}; - meshData.vertices = vertices.data(); - meshData.indices = indices->data(); - meshData.count = indices->size(); - meshData.new_indices.reserve(meshData.count); - - SMikkTSpaceInterface mts_interface{}; - mts_interface.m_getNumFaces = [](const SMikkTSpaceContext* pContext) -> int { - const MeshData* meshData = static_cast(pContext->m_pUserData); - assert(meshData->count % 3 == 0); - return meshData->count / 3; - }; - mts_interface.m_getNumVerticesOfFace = [](const SMikkTSpaceContext*, const int) -> int { return 3; }; - mts_interface.m_getPosition = [](const SMikkTSpaceContext* pContext, float fvPosOut[], const int iFace, const int iVert) -> void { - const MeshData* const meshData = static_cast(pContext->m_pUserData); - const glm::vec3 pos = meshData->vertices[meshData->indices[iFace * 3 + iVert]].pos; - fvPosOut[0] = pos.x; - fvPosOut[1] = pos.y; - fvPosOut[2] = pos.z; - }; - mts_interface.m_getNormal = [](const SMikkTSpaceContext* pContext, float fvNormOut[], const int iFace, const int iVert) -> void { - const MeshData* const meshData = static_cast(pContext->m_pUserData); - const glm::vec3 norm = meshData->vertices[meshData->indices[iFace * 3 + iVert]].norm; - fvNormOut[0] = norm.x; - fvNormOut[1] = norm.y; - fvNormOut[2] = norm.z; - }; - mts_interface.m_getTexCoord = [](const SMikkTSpaceContext* pContext, float fvTexcOut[], const int iFace, const int iVert) -> void { - const MeshData* const meshData = static_cast(pContext->m_pUserData); - const glm::vec2 uv = meshData->vertices[meshData->indices[iFace * 3 + iVert]].uv; - fvTexcOut[0] = uv.x; - fvTexcOut[1] = uv.y; - }; - mts_interface.m_setTSpaceBasic = [](const SMikkTSpaceContext* pContext, const float fvTangent[], const float fSign, const int iFace, - const int iVert) -> void { - MeshData* const meshData = static_cast(pContext->m_pUserData); - glm::vec4& tangent = meshData->vertices[meshData->indices[iFace * 3 + iVert]].tangent; - tangent.x = fvTangent[0]; - tangent.y = fvTangent[1]; - tangent.z = fvTangent[2]; - tangent.w = fSign; - }; - SMikkTSpaceContext mts_context{}; - mts_context.m_pInterface = &mts_interface; - mts_context.m_pUserData = &meshData; - - bool tan_result = genTangSpaceDefault(&mts_context); - if (tan_result == false) throw std::runtime_error("Failed to generate tangents!"); - } - - auto mesh_comp = app_scene.AddComponent(entity); - mesh_comp->mesh = std::make_unique(app_scene.app()->renderer()->GetDevice(), vertices, *indices); - - // now get material - mesh_comp->material = std::make_unique(app_scene.app()->renderer(), app_scene.app()->GetResource("builtin.fancy")); - - mesh_comp->material->SetAlbedoTexture(app_scene.app()->GetResource("builtin.white")); - mesh_comp->material->SetNormalTexture(app_scene.app()->GetResource("builtin.normal")); - - if (prim.material >= 0) { - const tg::Material& mat = gl_model.materials.at(prim.material); - if (mat.alphaMode != "OPAQUE") { - LOG_WARN("Non-opaque alpha modes are not supported yet"); - } - if (mat.doubleSided == true) { - LOG_WARN("Double-sided materials are not supported yet"); - } - if (mat.normalTexture.index != -1) { - if (mat.normalTexture.texCoord == 0) { - if (mat.normalTexture.scale == 1.0) { - const tg::Texture& norm_texture = gl_model.textures.at(mat.normalTexture.index); - if (norm_texture.source != -1) { - const tg::Image& norm_image = gl_model.images.at(norm_texture.source); - if (norm_image.as_is == false && norm_image.bits == 8 && norm_image.pixel_type == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { - // create texture on GPU - mesh_comp->material->SetNormalTexture(std::make_unique(app_scene.app()->renderer(), norm_image.image.data(), - norm_image.width, norm_image.height, - Texture::Filtering::kAnisotropic, false)); - } - } - } - else { - LOG_WARN("Normal texture has scaling which is unsupported. Ignoring normal map."); - } - } - else { - LOG_WARN("Normal texture doesn't specify UV0. Ignoring normal map."); - } - } - if (mat.pbrMetallicRoughness.baseColorTexture.index != -1) { - if (mat.pbrMetallicRoughness.baseColorTexture.texCoord == 0) { - const tg::Texture& texture = gl_model.textures.at(mat.pbrMetallicRoughness.baseColorTexture.index); - if (texture.source != -1) { - const tg::Image& image = gl_model.images.at(texture.source); - if (image.as_is == false && image.bits == 8 && image.pixel_type == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { - // create texture on GPU - mesh_comp->material->SetAlbedoTexture(std::make_unique(app_scene.app()->renderer(), image.image.data(), image.width, - image.height, Texture::Filtering::kAnisotropic, true)); - } - } - } - else { - LOG_WARN("Color texture doesn't specify UV0. Ignoring."); - } - } - } - } - - for (const int node : node.children) { - CreateNodes(app_scene, gl_scene, gl_model, entity, gl_model.nodes.at(node)); - } -} - engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) { @@ -333,13 +89,90 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) const tg::Scene& s = model.scenes.at(scene_index); + /* load all textures found in the model */ + + std::vector> textures{}; + textures.reserve(model.textures.size()); + + for (const tg::Texture& texture : model.textures) { + // find the image first + // use missing texture image by default + textures.emplace_back(scene.app()->GetResource("builtin.white")); + + if (texture.source == -1) continue; + + gfx::SamplerInfo samplerInfo{}; + // default to trilinear filtering even if mipmaps are not specified + samplerInfo.minify = gfx::Filter::kLinear; + samplerInfo.magnify = gfx::Filter::kLinear; + samplerInfo.mipmap = gfx::Filter::kLinear; + if (texture.sampler != -1) { + const tg::Sampler& sampler = model.samplers.at(texture.sampler); + switch (sampler.minFilter) { + case TINYGLTF_TEXTURE_FILTER_NEAREST: + case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR: + samplerInfo.minify = gfx::Filter::kNearest; + samplerInfo.mipmap = gfx::Filter::kLinear; + break; + case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST: + samplerInfo.minify = gfx::Filter::kNearest; + samplerInfo.mipmap = gfx::Filter::kNearest; + break; + case TINYGLTF_TEXTURE_FILTER_LINEAR: + case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR: + samplerInfo.minify = gfx::Filter::kLinear; + samplerInfo.mipmap = gfx::Filter::kLinear; + break; + case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST: + samplerInfo.minify = gfx::Filter::kLinear; + samplerInfo.mipmap = gfx::Filter::kNearest; + break; + default: + break; + } + switch (sampler.magFilter) { + case TINYGLTF_TEXTURE_FILTER_NEAREST: + samplerInfo.magnify = gfx::Filter::kNearest; + break; + case TINYGLTF_TEXTURE_FILTER_LINEAR: + samplerInfo.magnify = gfx::Filter::kLinear; + break; + default: + break; + } + } + // use aniso if min filter is LINEAR_MIPMAP_LINEAR + samplerInfo.anisotropic_filtering = (samplerInfo.minify == gfx::Filter::kLinear && samplerInfo.mipmap == gfx::Filter::kLinear); + + const tg::Image& image = model.images.at(texture.source); + if (image.as_is == false && image.bits == 8 && image.component == 4 && image.pixel_type == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { + // create texture on GPU + textures.back() = std::make_shared(scene.app()->renderer(), image.image.data(), image.width, + image.height, samplerInfo, true); + } + } + + /* load all materials found in model */ + + std::vector> materials{}; + materials.reserve(model.materials.size()); + for (const tg::Material& material : model.materials) { + // use default material unless a material is found + materials.emplace_back(scene.app()->GetResource("builtin.default")); + } + + /* load all meshes found in model */ + + std::vector> meshes{}; + meshes.reserve(model.meshes.size()); + for (const tg::Mesh& mesh : model.meshes) { + // placeholder mesh for now + + } + const Entity parent = scene.CreateEntity("test_node", 0, glm::vec3{}, glm::quat{glm::one_over_root_two(), glm::one_over_root_two(), 0.0f, 0.0f}); - for (int node : s.nodes) { - CreateNodes(scene, s, model, parent, model.nodes.at(node)); - } - return parent; } diff --git a/src/util/model_loader.cpp b/src/util/model_loader.cpp index 5238bae..c4b6f97 100644 --- a/src/util/model_loader.cpp +++ b/src/util/model_loader.cpp @@ -165,7 +165,7 @@ Entity LoadMeshFromFile(Scene* parent, const std::string& path, bool is_static) absPath = absPath.parent_path(); absPath /= texPath.C_Str(); try { - textures[i] = LoadTextureFromFile(absPath.string(), Texture::Filtering::kTrilinear, parent->app()->renderer()); + textures[i] = LoadTextureFromFile(absPath.string(), gfx::SamplerInfo{}, parent->app()->renderer()); } catch (const std::runtime_error&) { textures[i] = parent->app()->GetResource("builtin.white"); diff --git a/test/src/game.cpp b/test/src/game.cpp index 06284ce..f01d7f4 100644 --- a/test/src/game.cpp +++ b/test/src/game.cpp @@ -178,9 +178,9 @@ void PlayGame(GameSettings settings) wall_renderable->material = std::make_unique(app.renderer(), app.GetResource("builtin.fancy")); std::shared_ptr albedo_texture = - engine::LoadTextureFromFile(app.GetResourcePath("textures/brickwall_albedo.jpg"), engine::Texture::Filtering::kAnisotropic, app.renderer()); + engine::LoadTextureFromFile(app.GetResourcePath("textures/brickwall_albedo.jpg"), engine::gfx::SamplerInfo{}, app.renderer()); std::shared_ptr normal_texture = - engine::LoadTextureFromFile(app.GetResourcePath("textures/brickwall_normal.jpg"), engine::Texture::Filtering::kAnisotropic, app.renderer(), false); + engine::LoadTextureFromFile(app.GetResourcePath("textures/brickwall_normal.jpg"), engine::gfx::SamplerInfo{}, app.renderer(), false); wall_renderable->material->SetAlbedoTexture(albedo_texture); wall_renderable->material->SetNormalTexture(normal_texture);