ALlow texture filtering to be changed

This commit is contained in:
Bailey Harrison 2023-01-26 21:17:07 +00:00
parent ea51d669d9
commit 6e45aba65b
8 changed files with 136 additions and 59 deletions

View File

@ -35,6 +35,12 @@ namespace engine::gfx {
NEAREST, NEAREST,
}; };
enum class MipmapSetting {
OFF,
NEAREST,
LINEAR,
};
struct VertexBufferDesc { struct VertexBufferDesc {
uint64_t size; uint64_t size;
}; };

View File

@ -35,7 +35,14 @@ namespace engine {
gfx::Buffer* createBuffer(gfx::BufferType type, uint64_t size, const void* data); gfx::Buffer* createBuffer(gfx::BufferType type, uint64_t size, const void* data);
void destroyBuffer(const gfx::Buffer* buffer); void destroyBuffer(const gfx::Buffer* buffer);
gfx::Texture* createTexture(const void* imageData, uint32_t w, uint32_t h, gfx::TextureFilter minFilter, gfx::TextureFilter magFilter); gfx::Texture* createTexture(
const void* imageData,
uint32_t width,
uint32_t height,
gfx::TextureFilter minFilter,
gfx::TextureFilter magFilter,
gfx::MipmapSetting mipmapSetting,
bool useAnisotropy = false);
void destroyTexture(const gfx::Texture* texture); void destroyTexture(const gfx::Texture* texture);
// wait until all the active GPU queues have finished working // wait until all the active GPU queues have finished working

View File

@ -9,7 +9,14 @@ namespace engine::resources {
class Texture { class Texture {
public: public:
Texture(GFXDevice* gfxDevice, const std::string& path); enum class Filtering {
OFF,
BILINEAR,
TRILINEAR,
ANISOTROPIC,
};
Texture(GFXDevice* gfxDevice, const std::string& path, Filtering filtering, bool useMipmaps, bool useLinearMagFilter);
~Texture(); ~Texture();
Texture(const Texture&) = delete; Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete; Texture& operator=(const Texture&) = delete;

View File

@ -67,17 +67,17 @@ namespace engine {
m_resourcesPath = getResourcesPath(); m_resourcesPath = getResourcesPath();
// register resource managers // register resource managers
registerResourceManager<engine::resources::Texture>(); registerResourceManager<resources::Texture>();
registerResourceManager<engine::resources::Shader>(); registerResourceManager<resources::Shader>();
registerResourceManager<engine::resources::Material>(); registerResourceManager<resources::Material>();
registerResourceManager<engine::resources::Mesh>(); registerResourceManager<resources::Mesh>();
// default resources // default resources
{ {
resources::Shader::VertexParams vertParams{}; resources::Shader::VertexParams vertParams{};
vertParams.hasNormal = true; vertParams.hasNormal = true;
vertParams.hasUV0 = true; vertParams.hasUV0 = true;
auto texturedShader = std::make_unique<engine::resources::Shader>( auto texturedShader = std::make_unique<resources::Shader>(
gfx(), gfx(),
getResourcePath("engine/shaders/texture.vert").c_str(), getResourcePath("engine/shaders/texture.vert").c_str(),
getResourcePath("engine/shaders/texture.frag").c_str(), getResourcePath("engine/shaders/texture.frag").c_str(),
@ -85,14 +85,17 @@ namespace engine {
false, false,
true true
); );
getResourceManager<engine::resources::Shader>()->addPersistent("engine.textured", std::move(texturedShader)); getResourceManager<resources::Shader>()->addPersistent("engine.textured", std::move(texturedShader));
} }
{ {
auto whiteTexture = std::make_unique<engine::resources::Texture>( auto whiteTexture = std::make_unique<resources::Texture>(
gfx(), gfx(),
getResourcePath("engine/textures/white.png") getResourcePath("engine/textures/white.png"),
resources::Texture::Filtering::OFF,
false,
false
); );
getResourceManager<engine::resources::Texture>()->addPersistent("engine.white", std::move(whiteTexture)); getResourceManager<resources::Texture>()->addPersistent("engine.white", std::move(whiteTexture));
} }
} }

View File

@ -488,7 +488,6 @@ namespace engine {
static VkSampleCountFlagBits getMaxSampleCount(VkPhysicalDevice physicalDevice) static VkSampleCountFlagBits getMaxSampleCount(VkPhysicalDevice physicalDevice)
{ {
VkPhysicalDeviceProperties physicalDeviceProperties; VkPhysicalDeviceProperties physicalDeviceProperties;
vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties); vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);
@ -500,7 +499,7 @@ namespace engine {
if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; } if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; }
if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; } if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; }
if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; } if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; }
return VK_SAMPLE_COUNT_1_BIT; throw std::runtime_error("MSAA is not supported");
} }
// This is called not just on initialisation, but also when the window is resized. // This is called not just on initialisation, but also when the window is resized.
@ -636,6 +635,7 @@ namespace engine {
res = vkGetSwapchainImagesKHR(device, swapchain->swapchain, &swapchainImageCount, swapchain->images.data()); res = vkGetSwapchainImagesKHR(device, swapchain->swapchain, &swapchainImageCount, swapchain->images.data());
assert(res == VK_SUCCESS); assert(res == VK_SUCCESS);
// Use multisample anti-aliasing
swapchain->msaaSamples = getMaxSampleCount(physicalDevice); swapchain->msaaSamples = getMaxSampleCount(physicalDevice);
// create depth buffer if old depth buffer is wrong size. // create depth buffer if old depth buffer is wrong size.
@ -1004,6 +1004,8 @@ namespace engine {
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE; VkDevice device = VK_NULL_HANDLE;
Swapchain swapchain{};
std::vector<Queue> queues{}; std::vector<Queue> queues{};
Queue gfxQueue{}; Queue gfxQueue{};
Queue presentQueue{}; Queue presentQueue{};
@ -1011,20 +1013,17 @@ namespace engine {
VmaAllocator allocator = nullptr; VmaAllocator allocator = nullptr;
// device settings
bool vsync = false; bool vsync = false;
float maxSamplerAnisotropy;
Swapchain swapchain{}; // render loop
uint64_t FRAMECOUNT = 0; uint64_t FRAMECOUNT = 0;
std::array<VkCommandBuffer, FRAMES_IN_FLIGHT> commandBuffers{}; std::array<VkCommandBuffer, FRAMES_IN_FLIGHT> commandBuffers{};
std::array<VkFence, FRAMES_IN_FLIGHT> inFlightFences{}; std::array<VkFence, FRAMES_IN_FLIGHT> inFlightFences{};
std::queue<DrawCall> drawQueue{}; std::queue<DrawCall> drawQueue{};
VkDescriptorSetLayoutBinding uboLayoutBinding{}; VkDescriptorSetLayoutBinding uboLayoutBinding{};
VkDescriptorSetLayout descriptorSetLayout{}; VkDescriptorSetLayout descriptorSetLayout{};
VkDescriptorSetLayoutBinding samplerLayoutBinding{}; VkDescriptorSetLayoutBinding samplerLayoutBinding{};
VkDescriptorSetLayout samplerSetLayout{}; VkDescriptorSetLayout samplerSetLayout{};
@ -1043,14 +1042,13 @@ namespace engine {
// initialise vulkan // initialise vulkan
res = volkInitialize(); res = volkInitialize();
if (res == VK_ERROR_INITIALIZATION_FAILED) { if (res != VK_SUCCESS) {
throw std::runtime_error("Unable to load vulkan, is it installed?"); throw std::runtime_error("Unable to load vulkan, is it installed?");
} }
assert(res == VK_SUCCESS);
uint32_t vulkanVersion = volkGetInstanceVersion(); uint32_t vulkanVersion = volkGetInstanceVersion();
if (vulkanVersion < VK_MAKE_VERSION(1, 3, 0)) { assert(vulkanVersion != 0);
if (vulkanVersion < VK_API_VERSION_1_3) {
throw std::runtime_error("The loaded Vulkan version must be at least 1.3"); throw std::runtime_error("The loaded Vulkan version must be at least 1.3");
} }
@ -1122,17 +1120,16 @@ namespace engine {
#ifndef NDEBUG
for (const char* ext : extensions) { for (const char* ext : extensions) {
TRACE("Using Vulkan instance extension: {}", ext); DEBUG("Using Vulkan instance extension: {}", ext);
} }
#endif
res = vkCreateInstance(&instanceInfo, nullptr, &pimpl->instance); res = vkCreateInstance(&instanceInfo, nullptr, &pimpl->instance);
if (res == VK_ERROR_INCOMPATIBLE_DRIVER) { if (res == VK_ERROR_INCOMPATIBLE_DRIVER) {
throw std::runtime_error("The graphics driver is incompatible with vulkan"); throw std::runtime_error("The graphics driver is incompatible with vulkan");
} else if (res != VK_SUCCESS) {
throw std::runtime_error("vkCreateInstance failed: " + std::to_string(res));
} }
assert(res == VK_SUCCESS);
@ -1145,9 +1142,11 @@ namespace engine {
{ {
VkDebugUtilsMessengerCreateInfoEXT createInfo = getDebugMessengerCreateInfo(); VkDebugUtilsMessengerCreateInfoEXT createInfo = getDebugMessengerCreateInfo();
[[maybe_unused]] VkResult res; VkResult res;
res = vkCreateDebugUtilsMessengerEXT(pimpl->instance, &createInfo, nullptr, &pimpl->debugMessenger); res = vkCreateDebugUtilsMessengerEXT(pimpl->instance, &createInfo, nullptr, &pimpl->debugMessenger);
assert(res == VK_SUCCESS); if (res != VK_SUCCESS) {
throw std::runtime_error("vkCreateDebugUtilsMessengerExt failed: " + std::to_string(res));
}
} }
@ -1188,7 +1187,7 @@ namespace engine {
res = vkEnumerateDeviceExtensionProperties(dev, nullptr, &extensionCount, availableExtensions.data()); res = vkEnumerateDeviceExtensionProperties(dev, nullptr, &extensionCount, availableExtensions.data());
assert(res == VK_SUCCESS); assert(res == VK_SUCCESS);
for (const auto& extToFind : requiredDeviceExtensions) { for (const char* extToFind : requiredDeviceExtensions) {
bool extFound = false; bool extFound = false;
for (const auto& ext : availableExtensions) { for (const auto& ext : availableExtensions) {
if (strcmp(extToFind, ext.extensionName) == 0) { if (strcmp(extToFind, ext.extensionName) == 0) {
@ -1216,6 +1215,7 @@ namespace engine {
vkGetPhysicalDeviceFeatures(dev, &devFeatures); vkGetPhysicalDeviceFeatures(dev, &devFeatures);
// anisotropic filtering is needed // anisotropic filtering is needed
if (devFeatures.samplerAnisotropy == VK_FALSE) continue; if (devFeatures.samplerAnisotropy == VK_FALSE) continue;
pimpl->maxSamplerAnisotropy = devProps.limits.maxSamplerAnisotropy;
// check for linear filtering for mipmaps // check for linear filtering for mipmaps
VkFormatProperties formatProperties{}; VkFormatProperties formatProperties{};
@ -1342,7 +1342,6 @@ namespace engine {
} }
pimpl->presentQueue = getQueueSupporting(pimpl->queues, QueueFlags::TRANSFER); pimpl->presentQueue = getQueueSupporting(pimpl->queues, QueueFlags::TRANSFER);
pimpl->gfxQueue = getQueueSupporting(pimpl->queues, QueueFlags::GRAPHICS); pimpl->gfxQueue = getQueueSupporting(pimpl->queues, QueueFlags::GRAPHICS);
VkCommandPoolCreateInfo gfxCmdPoolInfo{ VkCommandPoolCreateInfo gfxCmdPoolInfo{
@ -2064,15 +2063,26 @@ namespace engine {
delete buffer; delete buffer;
} }
gfx::Texture* GFXDevice::createTexture(const void* imageData, uint32_t w, uint32_t h, gfx::TextureFilter minFilter, gfx::TextureFilter magFilter) gfx::Texture* GFXDevice::createTexture(
const void* imageData,
uint32_t width,
uint32_t height,
gfx::TextureFilter minFilter,
gfx::TextureFilter magFilter,
gfx::MipmapSetting mipmapSetting,
bool useAnisotropy)
{ {
auto out = new gfx::Texture; auto out = new gfx::Texture;
[[maybe_unused]] VkResult res; [[maybe_unused]] VkResult res;
size_t imageSize = w * h * 4; size_t imageSize = width * height * 4;
out->mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(w, h)))) + 1; if (mipmapSetting == gfx::MipmapSetting::OFF) {
out->mipLevels = 1;
} else {
out->mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1;
}
// first load image into staging buffer // first load image into staging buffer
VkBuffer stagingBuffer; VkBuffer stagingBuffer;
@ -2104,8 +2114,8 @@ namespace engine {
VkImageCreateInfo imageInfo{}; VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D; imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = w; imageInfo.extent.width = width;
imageInfo.extent.height = h; imageInfo.extent.height = height;
imageInfo.extent.depth = 1; imageInfo.extent.depth = 1;
imageInfo.mipLevels = out->mipLevels; imageInfo.mipLevels = out->mipLevels;
imageInfo.arrayLayers = 1; imageInfo.arrayLayers = 1;
@ -2140,14 +2150,14 @@ namespace engine {
region.imageSubresource.baseArrayLayer = 0; region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = 1;
region.imageOffset = { 0, 0, 0 }; region.imageOffset = { 0, 0, 0 };
region.imageExtent.width = w; region.imageExtent.width = width;
region.imageExtent.height = h; region.imageExtent.height = height;
region.imageExtent.depth = 1; region.imageExtent.depth = 1;
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, out->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region); vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, out->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
// Mipmap generation handles the transition to SHADER_READ_ONLY_OPTIMAL // Mipmap generation handles the transition to SHADER_READ_ONLY_OPTIMAL
cmdGenerateMipmaps(commandBuffer, out->image, w, h, out->mipLevels); cmdGenerateMipmaps(commandBuffer, out->image, width, height, out->mipLevels);
// end cmd buffer // end cmd buffer
endOneTimeCommands(pimpl->device, pimpl->commandPool, commandBuffer, pimpl->gfxQueue.handle); endOneTimeCommands(pimpl->device, pimpl->commandPool, commandBuffer, pimpl->gfxQueue.handle);
@ -2174,12 +2184,12 @@ namespace engine {
res = vkCreateImageView(pimpl->device, &imageViewInfo, nullptr, &out->imageView); res = vkCreateImageView(pimpl->device, &imageViewInfo, nullptr, &out->imageView);
assert(res == VK_SUCCESS); assert(res == VK_SUCCESS);
VkFilter magFilterInternal = vkinternal::getTextureFilter(magFilter);
VkFilter minFilterInternal = vkinternal::getTextureFilter(minFilter);
// create texture sampler // create texture sampler
{ {
VkFilter magFilterInternal = vkinternal::getTextureFilter(magFilter);
VkFilter minFilterInternal = vkinternal::getTextureFilter(minFilter);
VkSamplerCreateInfo samplerInfo{}; VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = magFilterInternal; samplerInfo.magFilter = magFilterInternal;
@ -2187,19 +2197,21 @@ namespace engine {
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.anisotropyEnable = VK_FALSE; if (useAnisotropy) {
if (magFilterInternal == VK_FILTER_LINEAR && minFilterInternal == VK_FILTER_LINEAR) {
// Find max anisotropic filtering level
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(pimpl->physicalDevice, &properties);
samplerInfo.anisotropyEnable = VK_TRUE; samplerInfo.anisotropyEnable = VK_TRUE;
samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; } else {
samplerInfo.anisotropyEnable = VK_FALSE;
} }
samplerInfo.maxAnisotropy = pimpl->maxSamplerAnisotropy;
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
samplerInfo.unnormalizedCoordinates = VK_FALSE; samplerInfo.unnormalizedCoordinates = VK_FALSE;
samplerInfo.compareEnable = VK_FALSE; samplerInfo.compareEnable = VK_FALSE;
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; if (mipmapSetting == gfx::MipmapSetting::LINEAR) {
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
} else {
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
}
samplerInfo.minLod = 0.0f; samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = static_cast<float>(out->mipLevels); samplerInfo.maxLod = static_cast<float>(out->mipLevels);
samplerInfo.mipLodBias = 0.0f; samplerInfo.mipLodBias = 0.0f;

View File

@ -7,19 +7,54 @@
namespace engine::resources { namespace engine::resources {
Texture::Texture(GFXDevice* gfxDevice, const std::string& path) Texture::Texture(GFXDevice* gfxDevice, const std::string& path, Filtering filtering, bool useMipmaps, bool useLinearMagFilter)
: m_gfxDevice(gfxDevice) : m_gfxDevice(gfxDevice)
{ {
int width, height; int width, height;
auto texbuf = util::readImageFile(path, &width, &height); auto texbuf = util::readImageFile(path, &width, &height);
gfx::TextureFilter filter = gfx::TextureFilter::LINEAR; gfx::TextureFilter minFilter, magFilter;
if (width <= 8 || height <= 8) { gfx::MipmapSetting mipmapSetting;
filter = gfx::TextureFilter::NEAREST; bool anisotropyEnable;
if (useLinearMagFilter) {
magFilter = gfx::TextureFilter::LINEAR;
} else {
magFilter = gfx::TextureFilter::NEAREST;
} }
m_gpuTexture = m_gfxDevice->createTexture(texbuf->data(), (uint32_t)width, (uint32_t)height, gfx::TextureFilter::LINEAR, filter); switch (filtering) {
case Filtering::OFF:
minFilter = gfx::TextureFilter::NEAREST;
mipmapSetting = gfx::MipmapSetting::NEAREST;
anisotropyEnable = false;
break;
case Filtering::BILINEAR:
minFilter = gfx::TextureFilter::LINEAR;
mipmapSetting = gfx::MipmapSetting::NEAREST;
anisotropyEnable = false;
break;
case Filtering::TRILINEAR:
minFilter = gfx::TextureFilter::LINEAR;
mipmapSetting = gfx::MipmapSetting::LINEAR;
anisotropyEnable = false;
break;
case Filtering::ANISOTROPIC:
minFilter = gfx::TextureFilter::LINEAR;
mipmapSetting = gfx::MipmapSetting::LINEAR;
anisotropyEnable = true;
}
if (useMipmaps == false) {
mipmapSetting = gfx::MipmapSetting::OFF;
}
m_gpuTexture = m_gfxDevice->createTexture(
texbuf->data(), (uint32_t)width, (uint32_t)height,
minFilter, magFilter,
mipmapSetting,
anisotropyEnable);
INFO("Loaded texture: {}, width: {} height: {}", path, width, height); INFO("Loaded texture: {}, width: {} height: {}", path, width, height);

View File

@ -148,11 +148,11 @@ namespace engine::util {
const char* errString = importer.GetErrorString(); const char* errString = importer.GetErrorString();
if (errString[0] != '\0' || scene == nullptr) { if (errString[0] != '\0' || scene == nullptr) {
throw std::runtime_error(errString); throw std::runtime_error("assimp error: " + std::string(errString));
} }
if (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) { if (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) {
throw std::runtime_error(errString); throw std::runtime_error("assimp error (incomplete): " + std::string(errString));
} }
assert(scene->HasAnimations() == false); assert(scene->HasAnimations() == false);
@ -181,7 +181,9 @@ namespace engine::util {
absPath = absPath.parent_path(); absPath = absPath.parent_path();
absPath /= texPath.C_Str(); absPath /= texPath.C_Str();
try { try {
textures[i] = std::make_shared<resources::Texture>(parent->app()->gfx(), absPath.string()); textures[i] = std::make_shared<resources::Texture>(
parent->app()->gfx(), absPath.string(),
resources::Texture::Filtering::TRILINEAR, true, true);
} catch (const std::runtime_error&) { } catch (const std::runtime_error&) {
textures[i] = parent->app()->getResource<resources::Texture>("engine.white"); textures[i] = parent->app()->getResource<resources::Texture>("engine.white");
} }

View File

@ -42,6 +42,8 @@ void playGame()
{ {
engine::Application app(PROJECT_NAME, PROJECT_VERSION); engine::Application app(PROJECT_NAME, PROJECT_VERSION);
app.setFrameLimiter(true);
// configure window // configure window
app.window()->setRelativeMouseMode(true); app.window()->setRelativeMouseMode(true);
@ -61,11 +63,14 @@ void playGame()
myScene->getSystem<engine::RenderSystem>()->setCameraEntity(camera); myScene->getSystem<engine::RenderSystem>()->setCameraEntity(camera);
// engine::util::loadMeshFromFile(myScene, app.getResourcePath("models/astronaut/astronaut.dae")); engine::util::loadMeshFromFile(myScene, app.getResourcePath("models/van/van.dae"));
auto grassTexture = std::make_shared<engine::resources::Texture>( auto grassTexture = std::make_shared<engine::resources::Texture>(
app.gfx(), app.gfx(),
app.getResourcePath("textures/grass.jpg") app.getResourcePath("textures/grass.jpg"),
engine::resources::Texture::Filtering::ANISOTROPIC,
true,
true
); );
uint32_t enemy = myScene->createEntity("enemy"); uint32_t enemy = myScene->createEntity("enemy");