From 9ec35a26e61977e77587790a7952ab2dc658d8a8 Mon Sep 17 00:00:00 2001 From: ecker Date: Thu, 14 Aug 2025 19:09:58 -0500 Subject: [PATCH] added texture/animation streaming (to-do: fix textures for OpenGL), serendipitously fixed animations when loading from a graph --- bin/data/entities/model.json | 2 +- engine/inc/uf/engine/graph/graph.h | 21 +++- engine/inc/uf/engine/graph/pod.inl | 1 + engine/inc/uf/ext/gltf/gltf.h | 8 +- engine/inc/uf/ext/opengl/texture.h | 3 +- engine/inc/uf/utils/image/image.h | 1 + engine/inc/uf/utils/mesh/mesh.h | 2 +- engine/src/engine/asset/asset.cpp | 3 +- engine/src/engine/graph/decode.cpp | 40 +++++-- engine/src/engine/graph/graph.cpp | 168 +++++++++++++++++++++++++++-- engine/src/ext/gltf/gltf.cpp | 13 +-- engine/src/ext/opengl/texture.cpp | 5 +- engine/src/utils/image/image.cpp | 3 + 13 files changed, 238 insertions(+), 32 deletions(-) diff --git a/bin/data/entities/model.json b/bin/data/entities/model.json index d7c5a36d..19b4f82a 100644 --- a/bin/data/entities/model.json +++ b/bin/data/entities/model.json @@ -79,7 +79,7 @@ "tag": "worldspawn", "player": "info_player_spawn", "enabled": "auto", - "radius": 16, + "radius": 32, "every": 4 } } diff --git a/engine/inc/uf/engine/graph/graph.h b/engine/inc/uf/engine/graph/graph.h index cb0327a6..ff0fa416 100644 --- a/engine/inc/uf/engine/graph/graph.h +++ b/engine/inc/uf/engine/graph/graph.h @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -46,7 +47,11 @@ namespace pod { uf::stl::vector skins; uf::stl::vector animations; // Animation queue - std::queue sequence; + uf::stl::queue sequence; + + // Streaming stuff + uf::stl::unordered_map buffer_paths; // probably will go unused since cramming it all in here is pain + struct { struct { bool loop = true; @@ -59,11 +64,16 @@ namespace pod { uf::stl::string target = ""; } animations; + struct { bool enabled = false; + float radius = 64.0f; float every = 4.0f; + bool textures = true; + bool animations = true; + uf::stl::string tag = "worldspawn"; uf::stl::string player = "info_player_spawn"; @@ -157,7 +167,14 @@ namespace uf { void UF_API destroy( uf::Object&, bool soft = false ); void UF_API destroy( pod::Graph::Storage&, bool soft = false ); - pod::Graph UF_API load( const uf::stl::string&, const uf::Serializer& = ext::json::null() ); + void UF_API load( pod::Graph&, const uf::stl::string&, const uf::Serializer& = ext::json::null() ); + inline pod::Graph load( const uf::stl::string& filename, const uf::Serializer& metadata = ext::json::null() ) { + // do some deprecation warning or something because this actually is bad for doing a copy + dealloc + pod::Graph graph; + load( graph, filename, metadata ); + return graph; + } + pod::Graph& UF_API convert( uf::Object&, bool = false ); uf::stl::string UF_API save( const pod::Graph&, const uf::stl::string& ); diff --git a/engine/inc/uf/engine/graph/pod.inl b/engine/inc/uf/engine/graph/pod.inl index 10a5c073..330de89e 100644 --- a/engine/inc/uf/engine/graph/pod.inl +++ b/engine/inc/uf/engine/graph/pod.inl @@ -72,6 +72,7 @@ namespace pod { }; uf::stl::string name = ""; + uf::stl::string path = ""; uf::stl::vector samplers; uf::stl::vector channels; diff --git a/engine/inc/uf/ext/gltf/gltf.h b/engine/inc/uf/ext/gltf/gltf.h index 2d190e9d..90490dda 100644 --- a/engine/inc/uf/ext/gltf/gltf.h +++ b/engine/inc/uf/ext/gltf/gltf.h @@ -9,7 +9,13 @@ namespace ext { namespace gltf { - pod::Graph UF_API load( const uf::stl::string&, const uf::Serializer& = {} ); + void UF_API load( pod::Graph&, const uf::stl::string&, const uf::Serializer& = {} ); + inline pod::Graph load( const uf::stl::string& filename, const uf::Serializer& metadata = ext::json::null() ) { + // do some deprecation warning or something because this actually is bad for doing a copy + dealloc + pod::Graph graph; + load( graph, filename, metadata ); + return graph; + } void UF_API save( const uf::stl::string&, const pod::Graph& ); } } diff --git a/engine/inc/uf/ext/opengl/texture.h b/engine/inc/uf/ext/opengl/texture.h index 6387b94b..a8563e71 100644 --- a/engine/inc/uf/ext/opengl/texture.h +++ b/engine/inc/uf/ext/opengl/texture.h @@ -53,6 +53,7 @@ namespace ext { enums::Format::type_t DefaultFormat = enums::Format::R8G8B8A8_UNORM; Device* device = nullptr; + bool aliased = false; GLuint image = GL_NULL_HANDLE; enums::Image::type_t type = enums::Image::TYPE_2D; @@ -81,7 +82,7 @@ namespace ext { void initialize( Device& device, enums::Image::viewType_t, size_t width, size_t height, size_t depth = 1, size_t layers = 1 ); #endif void updateDescriptors(); - void destroy(); + void destroy( bool = false ); bool generated() const; void loadFromFile( const uf::stl::string& filename, diff --git a/engine/inc/uf/utils/image/image.h b/engine/inc/uf/utils/image/image.h index 527d3572..7d7d66a8 100644 --- a/engine/inc/uf/utils/image/image.h +++ b/engine/inc/uf/utils/image/image.h @@ -33,6 +33,7 @@ namespace uf { void loadFromBuffer( const Image::container_t& container, const pod::Vector2ui& size, std::size_t bpp, std::size_t channels, bool flip = false ); uf::stl::string getFilename() const; + void setFilename( const uf::stl::string& ); Image::container_t& getPixels(); const Image::container_t& getPixels() const; diff --git a/engine/inc/uf/utils/mesh/mesh.h b/engine/inc/uf/utils/mesh/mesh.h index e21b1b43..aa4dd376 100644 --- a/engine/inc/uf/utils/mesh/mesh.h +++ b/engine/inc/uf/utils/mesh/mesh.h @@ -158,7 +158,7 @@ namespace uf { } bounds; */ uf::stl::vector buffers; - uf::stl::vector buffer_paths; // crunge + uf::stl::vector buffer_paths; // crunge, but it's better this way protected: void _destroy( uf::Mesh::Input& input ); void _bind( bool interleaved = uf::Mesh::defaultInterleaved ); diff --git a/engine/src/engine/asset/asset.cpp b/engine/src/engine/asset/asset.cpp index adc797c1..2946cff1 100644 --- a/engine/src/engine/asset/asset.cpp +++ b/engine/src/engine/asset/asset.cpp @@ -260,7 +260,8 @@ uf::stl::string uf::asset::load( uf::asset::Payload& payload ) { case uf::asset::Type::GRAPH: { UF_ASSET_REGISTER(pod::Graph) - asset = uf::graph::load( filename, payload.metadata ); + // asset = uf::graph::load( filename, payload.metadata ); + uf::graph::load( asset, filename, payload.metadata ); uf::graph::process( asset ); #if !UF_ENV_DREAMCAST diff --git a/engine/src/engine/graph/decode.cpp b/engine/src/engine/graph/decode.cpp index 6a1256db..e9cb9490 100644 --- a/engine/src/engine/graph/decode.cpp +++ b/engine/src/engine/graph/decode.cpp @@ -36,7 +36,11 @@ namespace { const uf::stl::string directory = uf::io::directory( graph.name ); const uf::stl::string filename = uf::io::filename( json["filename"].as() ); const uf::stl::string name = directory + "/" + filename; - image.open(name, false); + if ( graph.settings.stream.textures ) { + image.setFilename(name); + } else { + image.open(name, false); + } } else { auto size = uf::vector::decode( json["size"], pod::Vector2ui{} ); size_t bpp = json["bpp"].as(); @@ -245,7 +249,7 @@ namespace { #else if ( graph.metadata["stream"]["enabled"].as() ) { mesh.buffers.emplace_back(); - mesh.buffer_paths.emplace_back(directory + "/" + filename ); + mesh.buffer_paths.emplace_back(directory + "/" + filename); } else { // to-do: make it work for interleaved meshes mesh.buffers.emplace_back(uf::io::readAsBuffer( directory + "/" + filename )); @@ -335,13 +339,14 @@ namespace { } } -pod::Graph uf::graph::load( const uf::stl::string& filename, const uf::Serializer& metadata ) { +void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const uf::Serializer& metadata ) { const uf::stl::string extension = uf::io::extension( filename ); #if UF_USE_GLTF - if ( extension == "glb" || extension == "gltf" ) return ext::gltf::load( filename, metadata ); + if ( extension == "glb" || extension == "gltf" ) { + return ext::gltf::load( graph, filename, metadata ); + } #endif const uf::stl::string directory = uf::io::directory( filename ) + "/"; - pod::Graph graph; uf::Serializer serializer; UF_DEBUG_TIMER_MULTITRACE_START("Reading {}", filename); serializer.readFromFile( filename ); @@ -395,6 +400,10 @@ pod::Graph uf::graph::load( const uf::stl::string& filename, const uf::Serialize // copy important settings { graph.settings.stream.enabled = graph.metadata["stream"]["enabled"].as(graph.settings.stream.enabled); + + graph.settings.stream.textures = graph.settings.stream.enabled && graph.metadata["stream"]["textures"].as(graph.settings.stream.textures); + graph.settings.stream.animations = graph.settings.stream.enabled && graph.metadata["stream"]["animations"].as(graph.settings.stream.animations); + graph.settings.stream.radius = graph.metadata["stream"]["radius"].as(graph.settings.stream.radius); graph.settings.stream.every = graph.metadata["stream"]["every"].as(graph.settings.stream.every); @@ -565,18 +574,29 @@ pod::Graph uf::graph::load( const uf::stl::string& filename, const uf::Serialize /*graph.storage*/storage.animations.map.reserve( serializer["animations"].size() ); ext::json::forEach( serializer["animations"], [&]( ext::json::Value& value ){ if ( value.is() ) { + auto path = directory + "/" + value.as(); uf::Serializer json; - json.readFromFile( directory + "/" + value.as() ); + json.readFromFile( path ); auto name = key + json["name"].as(); - /*graph.storage*/storage.animations[name] = decodeAnimation( json, graph ); + if ( graph.settings.stream.animations ) { + /*graph.storage*/storage.animations[name].path = path; + } else { + /*graph.storage*/storage.animations[name] = decodeAnimation( json, graph ); + } + graph.animations.emplace_back(name); } else { // UF_MSG_DEBUG("{}", name); if ( value["filename"].is() ) { + auto path = directory + "/" + value.as(); uf::Serializer json; - json.readFromFile( directory + "/" + value["filename"].as() ); + json.readFromFile( path ); auto name = key + json["name"].as(); - /*graph.storage*/storage.animations[name] = decodeAnimation( json, graph ); + if ( graph.settings.stream.animations ) { + /*graph.storage*/storage.animations[name].path = path; + } else { + /*graph.storage*/storage.animations[name] = decodeAnimation( json, graph ); + } graph.animations.emplace_back(name); } else { auto name = key + value["name"].as(); @@ -631,6 +651,4 @@ pod::Graph uf::graph::load( const uf::stl::string& filename, const uf::Serialize #if UF_ENV_DREAMCAST DC_STATS(); #endif - - return graph; } \ No newline at end of file diff --git a/engine/src/engine/graph/graph.cpp b/engine/src/engine/graph/graph.cpp index bb58a1a7..81b63793 100644 --- a/engine/src/engine/graph/graph.cpp +++ b/engine/src/engine/graph/graph.cpp @@ -43,6 +43,59 @@ namespace { } return hash; } + + // lazy load animations if requested + void loadAnimation( const uf::stl::string& name ) { + auto& scene = uf::scene::getCurrentScene(); + auto& storage = uf::graph::globalStorage ? uf::graph::storage : scene.getComponent(); + + auto& animation = storage.animations.map[name]; + + UF_ASSERT( animation.path != "" ); + + uf::Serializer json; + json.readFromFile( animation.path ); + + animation.name = json["name"].as(animation.name); + animation.start = json["start"].as(animation.start); + animation.end = json["end"].as(animation.end); + + if ( animation.samplers.empty() ) ext::json::forEach( json["samplers"], [&]( ext::json::Value& value ){ + auto& sampler = animation.samplers.emplace_back(); + sampler.interpolator = value["interpolator"].as(sampler.interpolator); + + sampler.inputs.reserve( value["inputs"].size() ); + ext::json::forEach( value["inputs"], [&]( ext::json::Value& input ){ + sampler.inputs.emplace_back( input.as() ); + }); + + sampler.outputs.reserve( value["outputs"].size() ); + ext::json::forEach( value["outputs"], [&]( ext::json::Value& output ){ + sampler.outputs.emplace_back( uf::vector::decode( output, pod::Vector4f{} ) ); + }); + }); + + if ( animation.channels.empty() ) ext::json::forEach( json["channels"], [&]( ext::json::Value& value ){ + auto& channel = animation.channels.emplace_back(); + channel.path = value["path"].as(channel.path); + channel.node = value["node"].as(channel.node); + channel.sampler = value["sampler"].as(channel.sampler); + }); + } + + void unloadAnimation( const uf::stl::string& name ) { + auto& scene = uf::scene::getCurrentScene(); + auto& storage = uf::graph::globalStorage ? uf::graph::storage : scene.getComponent(); + + auto& animation = storage.animations.map[name]; + + animation.samplers.clear(); + animation.channels.clear(); + #if UF_ENV_DREAMCAST + animation.samplers.shrink_to_fit(); + animation.channels.shrink_to_fit(); + #endif + } } #if UF_ENV_DREAMCAST @@ -808,7 +861,6 @@ void uf::graph::process( pod::Graph& graph ) { uf::stl::unordered_map isSrgb; // process lightmap - UF_DEBUG_TIMER_MULTITRACE("Parsing lightmaps"); if ( true ) { constexpr const char* UF_GRAPH_DEFAULT_LIGHTMAP = "./lightmap.%i.png"; @@ -880,7 +932,9 @@ void uf::graph::process( pod::Graph& graph ) { auto& texture = /*graph.storage*/storage.textures[graph.textures.emplace_back(f)]; auto& image = /*graph.storage*/storage.images[graph.images.emplace_back(f)]; - image.open( f, false ); + if ( !graph.settings.stream.textures ) { + image.open( f, false ); + } texture.index = imageID; @@ -934,6 +988,12 @@ void uf::graph::process( pod::Graph& graph ) { auto& image = storage.images[key]; auto& texture = storage.texture2Ds[key]; if ( !texture.generated() ) { + // set as null + if ( graph.settings.stream.textures ) { + texture.aliasTexture(uf::renderer::Texture2D::empty); + continue; + } + auto filter = uf::renderer::enums::Filter::LINEAR; auto tag = ext::json::find( key, graph.metadata["tags"] ); if ( !ext::json::isObject( tag ) ) { @@ -948,7 +1008,7 @@ void uf::graph::process( pod::Graph& graph ) { texture.sampler.descriptor.filter.min = filter; texture.sampler.descriptor.filter.mag = filter; - texture.srgb = isSrgb.count(key) == 0 ? false : isSrgb[key]; + texture.srgb = isSrgb[key]; texture.loadFromImage( image ); #if UF_ENV_DREAMCAST @@ -1570,6 +1630,9 @@ void uf::graph::override( pod::Graph& graph ) { if ( !toNeutralPose ) { uf::stl::string name = graph.sequence.front(); pod::Animation& animation = storage.animations.map[name]; // graph.animations[name]; + // load animation data + if ( animation.channels.empty() || animation.samplers.empty() ) ::loadAnimation( name ); + for ( auto& channel : animation.channels ) { auto& override = graph.settings.animations.override.map[channel.node]; auto& sampler = animation.samplers[channel.sampler]; @@ -1615,7 +1678,11 @@ void uf::graph::animate( pod::Graph& graph, const uf::stl::string& _name, float // if already playing, ignore it if ( !graph.sequence.empty() && graph.sequence.front() == name ) return; if ( immediate ) { - while ( !graph.sequence.empty() ) graph.sequence.pop(); + while ( !graph.sequence.empty() ) { + // unload + if ( graph.settings.stream.animations ) ::unloadAnimation( graph.sequence.front() ); + graph.sequence.pop(); + } } bool empty = graph.sequence.empty(); graph.sequence.emplace(name); @@ -1678,7 +1745,10 @@ void uf::graph::update( pod::Graph& graph, float delta ) { animation->cur = graph.settings.animations.loop ? animation->cur - animation->end : 0; // go-to next animation if ( !graph.settings.animations.loop ) { + // unload + if ( graph.settings.stream.animations ) ::unloadAnimation( graph.sequence.front() ); graph.sequence.pop(); + // out of animations, set to neutral pose if ( graph.sequence.empty() ) { uf::graph::override( graph ); @@ -1688,6 +1758,10 @@ void uf::graph::update( pod::Graph& graph, float delta ) { animation = &storage.animations.map[name]; // &graph.animations[name]; } } + + // load animation data + if ( animation->channels.empty() || animation->samplers.empty() ) ::loadAnimation( name ); + for ( auto& channel : animation->channels ) { auto& sampler = animation->samplers[channel.sampler]; if ( sampler.interpolator != "LINEAR" ) continue; @@ -2131,7 +2205,87 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { mesh.buffers[attribute.buffer] = uf::io::readAsBuffer( mesh.buffer_paths[attribute.buffer] ); } #endif - } else { + + if ( graph.settings.stream.textures ) { + // cringe macro that ensures a texture ID is mapped properly, regardless if its visible or not + // lightmaps are not sRGB, while textures (usually) are + #define INCREMENT_TEXTURE_REFCOUNT( ID, isSRGB ) if ( 0 <= ID && ID < graph.textures.size() ) {\ + auto& key = graph.textures[ID];\ + textureReferences[key] += visible ? 1 : 0;\ + isSrgb[key] = isSRGB;\ + } + + uf::stl::unordered_map isSrgb; // cringe + uf::stl::unordered_map textureReferences; + // determine which textures are in use or not + for ( size_t drawID = 0; drawID < primitives.size(); ++drawID ) { + auto& primitive = primitives[drawID]; + auto& instance = primitive.instance; + auto& drawCommand = drawCommands[drawID]; + + bool visible = drawCommand.instances > 0; + + INCREMENT_TEXTURE_REFCOUNT(instance.lightmapID, false); + // no material information bound + if ( !(0 <= instance.materialID && instance.materialID < graph.materials.size()) ) { + continue; + } + auto& material = storage.materials[graph.materials[instance.materialID]]; + INCREMENT_TEXTURE_REFCOUNT(material.indexAlbedo, true); + INCREMENT_TEXTURE_REFCOUNT(material.indexNormal, true); + INCREMENT_TEXTURE_REFCOUNT(material.indexEmissive, true); + INCREMENT_TEXTURE_REFCOUNT(material.indexOcclusion, true); + INCREMENT_TEXTURE_REFCOUNT(material.indexMetallicRoughness, true); + } + + // iterate through our ref counts + // to-do: figure out why this doesn't work for OpenGL (texture ID handles might be wrong, might be better to store the old texture ID handle to use it and then update to that handle) + for ( auto& [ key, count ] : textureReferences ) { + auto& texture = storage.texture2Ds[key]; + auto& image = storage.images[key]; + bool visible = count > 0; + + // load texture + if ( visible && (!texture.generated() || texture.aliased) ) { + // load image + if ( image.getPixels().empty() ) image.open(image.getFilename(), false); + + auto filter = uf::renderer::enums::Filter::LINEAR; + auto tag = ext::json::find( key, graph.metadata["tags"] ); + if ( !ext::json::isObject( tag ) ) { + tag["renderer"] = graph.metadata["renderer"]; + } + if ( tag["renderer"]["filter"].is() ) { + const auto mode = uf::string::lowercase( tag["renderer"]["filter"].as("linear") ); + if ( mode == "linear" ) filter = uf::renderer::enums::Filter::LINEAR; + else if ( mode == "nearest" ) filter = uf::renderer::enums::Filter::NEAREST; + else UF_MSG_WARNING("Invalid Filter enum string specified: {}", mode); + } + + // avoids manipulating the aliased texture + if ( texture.aliased ) { + texture = {}; + } + + texture.sampler.descriptor.filter.min = filter; + texture.sampler.descriptor.filter.mag = filter; + texture.srgb = isSrgb[key]; + + texture.loadFromImage( image ); + #if UF_ENV_DREAMCAST + image.clear(); + #endif + } else if ( !visible && texture.generated() ) { + // unload image + image.clear(); + // defer destruction of texture + texture.destroy( true ); + // alias to null texture + texture.aliasTexture(uf::renderer::Texture2D::empty); + } + } + } + } else { // this shouldn't be reached // load mesh data for ( auto& attribute : mesh.index.attributes ) { if ( !mesh.buffers[attribute.buffer].empty() || mesh.buffer_paths.empty() ) continue; @@ -2144,8 +2298,8 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { } mesh.updateDescriptor(); - - // process textures + // may or may not be necessary (OpenGL might need a re-record, Vulkan seems fine for the main deferred pass but VXGI doesn't ever get to update since null textures get used sometimes) + uf::renderer::states::rebuild = true; // update graphic if ( /*(graph.metadata["renderer"]["separate"].as()) &&*/ graph.metadata["renderer"]["render"].as() ) { diff --git a/engine/src/ext/gltf/gltf.cpp b/engine/src/ext/gltf/gltf.cpp index 771f0101..f34bed0b 100644 --- a/engine/src/ext/gltf/gltf.cpp +++ b/engine/src/ext/gltf/gltf.cpp @@ -117,10 +117,10 @@ namespace { } } -pod::Graph ext::gltf::load( const uf::stl::string& filename, const uf::Serializer& metadata ) { +void ext::gltf::load( pod::Graph& graph, const uf::stl::string& filename, const uf::Serializer& metadata ) { uf::stl::string extension = uf::io::extension( filename ); if ( extension != "glb" && extension != "gltf" ) { - return uf::graph::load( filename, metadata ); + return uf::graph::load( graph, filename, metadata ); } tinygltf::Model model; @@ -129,14 +129,13 @@ pod::Graph ext::gltf::load( const uf::stl::string& filename, const uf::Serialize uf::stl::string warn, err; bool ret = extension == "glb" ? loader.LoadBinaryFromFile(&model, &err, &warn, filename) : loader.LoadASCIIFromFile(&model, &err, &warn, filename); - pod::Graph graph; graph.name = filename; graph.metadata = metadata; if ( !warn.empty() ) UF_MSG_WARNING("glTF warning: {}", warn); if ( !err.empty() ) UF_MSG_ERROR("glTF error: {}", err); if ( !ret ) { UF_MSG_ERROR("glTF error: failed to parse file: {}", filename); - return graph; + return; } #if 0 @@ -570,10 +569,12 @@ pod::Graph ext::gltf::load( const uf::stl::string& filename, const uf::Serialize // disable streaming { graph.settings.stream.enabled = false; + + graph.settings.stream.textures = false; + graph.settings.stream.animations = false; + graph.settings.stream.radius = 0; graph.settings.stream.every = 0; } - - return graph; } #endif \ No newline at end of file diff --git a/engine/src/ext/opengl/texture.cpp b/engine/src/ext/opengl/texture.cpp index 832225f0..e5b20281 100644 --- a/engine/src/ext/opengl/texture.cpp +++ b/engine/src/ext/opengl/texture.cpp @@ -70,8 +70,10 @@ bool ext::opengl::Texture::generated() const { // return glIsTexture(image); #endif } -void ext::opengl::Texture::destroy() { +void ext::opengl::Texture::destroy( bool defer ) { // if ( !device ) return; + if ( aliased ) return; + if ( generated() ) { GL_MUTEX_LOCK(); GL_ERROR_CHECK(glDeleteTextures(1, &image)); @@ -264,6 +266,7 @@ ext::opengl::Texture ext::opengl::Texture::alias() const { return texture; } void ext::opengl::Texture::aliasTexture( const Texture& texture ) { + aliased = true; image = texture.image; type = texture.type; viewType = texture.viewType; diff --git a/engine/src/utils/image/image.cpp b/engine/src/utils/image/image.cpp index 797d7d62..d30c1e79 100644 --- a/engine/src/utils/image/image.cpp +++ b/engine/src/utils/image/image.cpp @@ -76,6 +76,9 @@ uf::Image::Image( const Image::container_t& copy, const Image::vec2_t& size ) : uf::stl::string uf::Image::getFilename() const { return this->m_filename; } +void uf::Image::setFilename( const uf::stl::string& filename ) { + this->m_filename = filename; +} #define _PACK4(v) ((v * 0xF) / 0xFF) #define PACK_ARGB4444(a,r,g,b) (_PACK4(a) << 12) | (_PACK4(r) << 8) | (_PACK4(g) << 4) | (_PACK4(b))