• RuntimesGodot
  • Loading Spine Sprite at runtime

Hello,

I am working on a project and as part of that building and editor. I need to be able to load a SpineSprite from it's files directly not just only after an import.

func create_spine_sprite()->SpineSprite:
	print("Create spine atlas=",atlas," spine_json="+spine_json)
	var atlas_res:SpineAtlasResource=SpineAtlasResource.new()
	var error:Error=atlas_res.load_from_atlas_file(atlas)
	
	if error != OK:
		printerr("Failure! ",error)
		return null
	
	var skeleton_file_res:SpineSkeletonFileResource=SpineSkeletonFileResource.new()
	skeleton_file_res.resource_path=spine_json
	
	var skeleton_data_res:SpineSkeletonDataResource=SpineSkeletonDataResource.new()	
	skeleton_data_res.skeleton_file_res=skeleton_file_res
	skeleton_data_res.atlas_res=atlas_res
	
	var sprite:SpineSprite=SpineSprite.new()
	sprite.skeleton_data_res=skeleton_data_res
	return sprite

Currently that is what I am attempting but the load_from_atlas_file method appears to be changing my path to a res: path and failing. At least that is what I can deduce from the error.

I imagine since the plugin loads and creates these resources from files the methods I need should be available (assuming they were exposed).

Is the C++ code available for me to read through searching for how to achieve this?
I found the github code

Related Discussions
...

What is the point of this method in SpineAtlasResource.cpp ?

	static String fix_path(const String &path) {
		if (path.size() > 5 && path[4] == '/' && path[5] == '/') return path;
		const String prefix = "res:/";
		auto i = path.find(prefix);
		auto sub_str_pos = i + prefix.size() - 1;
		if (sub_str_pos < 0) return path;
		auto res = path.substr(sub_str_pos);

		if (!EMPTY(res)) {
			if (res[0] != '/') {
				return prefix + "/" + res;
			} else {
				return prefix + res;
			}
		}
		return path;
	}

Assuming find returns a -1 when the string does not include "res:/" this will crop the string start off by the length of the prefix.

If the string starts with "res:/" then the function will return the path without the res:// however if the path does not start with "res:/" this will still return the path without the first 5 characters.

Should this function not have been.

	static String fix_path(const String &path) {
		if (path.size() > 5 && path[4] == '/' && path[5] == '/') return path;
		const String prefix = "res:/";
		auto i = path.find(prefix);
		auto sub_str_pos = i + prefix.size() - 1;
		if (i < 0) return path; // Return if the string was never found
		auto res = path.substr(sub_str_pos);

		if (!EMPTY(res)) {
			if (res[0] != '/') {
				return prefix + "/" + res;
			} else {
				return prefix + res;
			}
		}
		return path;
	}
  • Nate оценил это.

Oh my, sorry for the pain this caused. I've opened an issue here and will push this tomorrow.

EsotericSoftware/spine-runtimes2477

  • belshamo ответили на это сообщение.

    Mario

    Thank you Mario I really appreciate feedback and the raising of a ticket.

    I have checked the code out to am trying to get the build working some kinks still with the intention of working though loading from file.

    Is it expected that you can load from file sources not only from project resources?

    • Mario ответили на это сообщение.

      @Mario

      Hello I have completed the changes needed to load spine sprites from file. The path error was only one issue there is also the problem that the Resource loader for the images doesn't load images from path only from resources. So Image::load was needed.

      This does however introduce a new concern that each load will import the image from the drive as there is no access to the Resource Cache. I overcame this in GDScript for now. It would be better for this to be handled in the loader by at least keeping a WeakRef to any images and reuse them should they already exist.

      Is there any way i could ask for the c++ changes to go into the base code. I am happy to meet your development standards just let me know what I need to improve or change. It would just be simpler for me in the long run if I don't have to make this modification each time there is a new version.

      Changed to SpinAtlasResource.cpp

      GodotSpineTextureLoader::fix_path

      This was changed to resolve the error I mentioned earlier.

      	static String fix_path(const String &path) {
      		if (path.size() > 5 && path[4] == '/' && path[5] == '/') return path;
      		const String prefix = "res:/";
      		auto i = path.find(prefix);
      		if (i < 0) return path;
      		auto sub_str_pos = i + prefix.size() - 1;
      		auto res = path.substr(sub_str_pos);
      
      		if (!EMPTY(res)) {
      			if (res[0] != '/') {
      				return prefix + "/" + res;
      			} else {
      				return prefix + res;
      			}
      		}
      		return path;
      	}

      GodotSpineTextureLoader::load

      This had to change to facilitate loading the texture from disk. I only implemented the load for version 4.

      	void load(spine::AtlasPage &page, const spine::String &path) override {
      		Error error = OK;
      		auto fixed_path = fix_path(String(path.buffer()));
      
      #if VERSION_MAJOR > 3
      		const String prefix = "res:/";
      		auto i = fixed_path.find(prefix);
      		Ref<Texture2D> texture;
      		if (i < 0) {
      			Ref<Image> image=Image::load_from_file(fixed_path);
      			texture = ImageTexture::create_from_image(image);
      		} else {
      			texture = ResourceLoader::load(fixed_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &error);
      		}
      #else
      		Ref<Texture> texture = ResourceLoader::load(fixed_path, "", false, &error);
      #endif
      
      		if (error != OK || !texture.is_valid()) {
      			ERR_PRINT(vformat("Can't load texture: \"%s\"", String(path.buffer())));
      			auto renderer_object = memnew(SpineRendererObject);
      			renderer_object->texture = Ref<Texture>(nullptr);
      			renderer_object->normal_map = Ref<Texture>(nullptr);
      			page.texture = (void *) renderer_object;
      			return;
      		}
      
      		textures->append(texture);
      		auto renderer_object = memnew(SpineRendererObject);
      		renderer_object->texture = texture;
      		renderer_object->normal_map = Ref<Texture>(nullptr);
      
      		String new_path = vformat("%s/%s_%s", fixed_path.get_base_dir(), normal_map_prefix, fixed_path.get_file());
      		if (ResourceLoader::exists(new_path)) {
      			Ref<Texture> normal_map = ResourceLoader::load(new_path);
      			normal_maps->append(normal_map);
      			renderer_object->normal_map = normal_map;
      		}

      SpineSkeletonFileResource::_bind_methods

      Another gap was that load_from_file for skeleton was not bound to GDScript so it could not be called this is necessary to allow loading from disk.

      
      void SpineSkeletonFileResource::_bind_methods() {
      	ClassDB::bind_method(D_METHOD("load_from_file", "path"), &SpineSkeletonFileResource::load_from_file);
      	ADD_SIGNAL(MethodInfo("skeleton_file_changed"));
      }

      New GDScript to load from disk

      To overcome the issues of loading multiple atlases and skeletons into the system I built a helper class that will cache a WeakRef to both. This means if they are still in the scene then they will not be loaded again but they should not stay in ram any longer then that.

      extends Node
      class_name SpineSpriteFileLoader
      
      static var known_atlasses={}
      static var known_skeletons={}
      
      static func _load_spine_atlas(atlas_path:String)->SpineAtlasResource:
      	var atlas_res:SpineAtlasResource=null
      	if known_atlasses.has(atlas_path) and known_atlasses[atlas_path].get_ref() != null:
      		atlas_res=known_atlasses[atlas_path].get_ref()
      	else:
      		atlas_res=SpineAtlasResource.new()
      		var error:Error=atlas_res.load_from_atlas_file(atlas_path)
      		if error != OK:
      			printerr("Failure loading atlas@"+atlas_path,error)
      		known_atlasses[atlas_path]=weakref(atlas_res)
      	return atlas_res
      
      
      static func _load_spine_skeleton(skeleton_json_path:String)->SpineSkeletonFileResource:
      	var skeleton_file_res:SpineSkeletonFileResource=null
      	if known_skeletons.has(skeleton_json_path) and known_skeletons[skeleton_json_path].get_ref() != null:
      		skeleton_file_res=known_skeletons[skeleton_json_path].get_ref()
      	else:
      		skeleton_file_res=SpineSkeletonFileResource.new()
      		var error:Error=skeleton_file_res.load_from_file(skeleton_json_path)
      		if error != OK:
      			printerr("Failure loading json-spine! ",error)
      		known_skeletons[skeleton_json_path]=weakref(skeleton_file_res)
      	return skeleton_file_res
      
      
      static func load_spine_sprite(atlas_path:String,skeletonm_json_path:String)->SpineSprite:
      	var atlas_res:SpineAtlasResource=_load_spine_atlas(atlas_path)
      	var skeleton_file_res:SpineSkeletonFileResource=_load_spine_skeleton(skeletonm_json_path)
      
      	var skeleton_data_res:SpineSkeletonDataResource=SpineSkeletonDataResource.new()
      	skeleton_data_res.skeleton_file_res=skeleton_file_res
      	skeleton_data_res.atlas_res=atlas_res
      	
      	var sprite:SpineSprite=SpineSprite.new()
      	sprite.skeleton_data_res=skeleton_data_res
      	return sprite

      Best Regards
      Travis Bulford

      • Qugurun ответили на это сообщение.
      • Qugurun оценил это.

        belshamo it's currently not a supported use case to load from file resources at runtime.

        Your proposed changes look good. I did not yet get to incorporating your changes. I hope to be able to find time tomorrow.

        месяц спустя

        Hello,

        Is this something you will still be able to merge into the code base?

        Not for the upcoming Spine 4.2 release which we'll likely publish this week. However, if you folks can send a PR, I can see if I can get it into the next patch release. It will need quite some testing on my end, as it does modify a crucial code path.

        20 дней спустя

        belshamo
        forum

        I encountered the same issue. Could you please provide guidance on how to load a texture but only for Godot version 3.5.3, as the necessary methods are not present in the Godot *.cpp files?

        Ref<Image> image=Image::load_from_file(fixed_path);
        texture = ImageTexture::create_from_image(image);
        • belshamo ответили на это сообщение.

          I have figured out the image loading part, now I just need to turn it into a texture.

          old:
          Ref<Image> image=Image::load_from_file(fixed_path);

          new:

          Ref<Image> image;
          image.instance();
          ImageLoader::load_image(fixed_path, image);

          I was able to make a similar modification for version 3.5.3, and it works!!

          const String prefix = "res:/";
          auto i = fixed_path.find(prefix);
          Ref<ImageTexture> texture; 
          if (i < 0) {
          	Ref<Image> image;
          	image.instance();
          	image->load(fixed_path);
          	
          	texture.instance();
          	texture->create_from_image(image);
          } else {
          	texture = ResourceLoader::load(fixed_path, "", false, &error);
          }
          • Nate оценил это.

          Fix:

          const String prefix = "res:/";
          auto i = fixed_path.find(prefix);
           
          Ref<Texture> texture;
          if (i < 0) {
          	Ref<Image> image;
          	image.instance();
          	image->load(fixed_path);
          	
          	Ref<ImageTexture> image_texture;
          	image_texture.instance();
          	image_texture->create_from_image(image);
          	texture = image_texture;
          } else {
          	texture = ResourceLoader::load(fixed_path, "", false, &error);
          }

          Fix GDScript for Godot 3.x

          extends Node
          class_name SpineSpriteFileLoader
          
          const known_atlasses = {}
          const known_skeletons = {}
          
          static func _load_spine_atlas(atlas_path:String) -> SpineAtlasResource:
          	var atlas_res:SpineAtlasResource = null
          	if known_atlasses.has(atlas_path) and known_atlasses[atlas_path].get_ref() != null:
          		atlas_res = known_atlasses[atlas_path].get_ref()
          	else:
          		atlas_res = SpineAtlasResource.new()
          		var error = atlas_res.load_from_atlas_file(atlas_path)
          		if error != OK:
          			printerr("Failure loading atlas@"+atlas_path,error)
          		known_atlasses[atlas_path] = weakref(atlas_res)
          	return atlas_res
          
          
          static func _load_spine_skeleton(skeleton_json_path:String) -> SpineSkeletonFileResource:
          	var skeleton_file_res:SpineSkeletonFileResource = null
          	if known_skeletons.has(skeleton_json_path) and known_skeletons[skeleton_json_path].get_ref() != null:
          		skeleton_file_res = known_skeletons[skeleton_json_path].get_ref()
          	else:
          		skeleton_file_res = SpineSkeletonFileResource.new()
          		var error = skeleton_file_res.load_from_file(skeleton_json_path)
          		if error != OK:
          			printerr("Failure loading json-spine! ",error)
          		known_skeletons[skeleton_json_path] = weakref(skeleton_file_res)
          	return skeleton_file_res
          
          
          static func load_spine_sprite(atlas_path:String, skeletonm_json_path:String) -> SpineSprite:
          	var atlas_res:SpineAtlasResource = _load_spine_atlas(atlas_path)
          	var skeleton_file_res:SpineSkeletonFileResource=_load_spine_skeleton(skeletonm_json_path)
          
          	var skeleton_data_res:SpineSkeletonDataResource = SpineSkeletonDataResource.new()
          	skeleton_data_res.skeleton_file_res = skeleton_file_res
          	skeleton_data_res.atlas_res = atlas_res
          	
          	var sprite:SpineSprite = SpineSprite.new()
          	sprite.skeleton_data_res = skeleton_data_res
          	return sprite

          Thanks for the investigation. I'll be adding both of your suggestions to our build this week.

          Thanks Mario. I know you asked for a pull request. I can still do that but if you can manage without one I would appreciate it as I have never done one before and was still reading up.

          Qugurun glad you got it done. I skipped using 3 altogether new to Godot and started with 4.

          No need for a pull request, I'll manage to do it without one.

          2 месяца спустя

          @belshamo @Mario

          I've noticed some strange behavior, most likely it's an error that needs to be fixed, but unfortunately, I haven't been able to find a solution for it. I'm writing about it in this thread because it's related to it. If we have a scene with a spine object that was loaded from the file system rather than from the game's resources, then upon reloading, the spine object will be empty. If you have a quick solution for this, I would greatly appreciate it.

          5 дней спустя

          I'm afraid I do not have a quick solution. Can you please provide me with an example project to reproduce the issue? Just a scene + gdscript + assets would do.