Difficulty when loading images from external source (SOLVED)

I’m experimenting with modding and I’m trying to import an image from the save location on Windows (C:/Users/[user]/AppData/Roaming/[appname]) called arrow.png. I’m using the Image loader extension but I’m having some difficulties.

2 Main problems:

  1. Whenever I use resource.set_texture(), it sets the texture without changing image size on a sprite
  2. When the sprite is already in the right dimensions (it’s default animation is set on the same image I’m trying to load), it doesn’t appear when I load it, just shows a black box smaller than the expected size.

I don’t have an image right now, but it’s a 100x100 black box with a white arrow pointing right.

This is the code:

--testlua.lua
function loadResources()
    local path = sys.get_save_file("mpcreator", "arrow.png")
    local f = io.open(path, "rb")
    local bytes = f:read("*a")
    f:close()

    local image_resource = imageloader.load{
        data = bytes
    }

    resource.set_texture(go.get("#sprite", "texture0"), image_resource.header, image_resource.buffer)
end


--test.script
function init(self)
    local script = sys.load_resource("/res/testlua.lua")
    local f = assert(loadstring(script))
    f()
    loadResources()
end

I tried it directly inside the test.script file and the same thing happened.

Any help is appreciated, thanks.

Does it need to be sprites? This is easier to do with GUI nodes.

Example of loading splash images for portals in our games.

Edit: I realize you meant to say supporting game modding so this may not be super helpful.

local IMAGE_GUI = ""

local sys_info = sys.get_sys_info()
local files = require("imports.files")
local TEXTURE_ID_1 = "TEXTURE_ID_1"
local TEXTURE_ID_2 = "TEXTURE_ID_2"

local function is_editor()
	if not (sys_info.system_name == "Windows" or sys_info.system_name == "Darwin" or sys_info.system_name == "Linux") then
		return false
	end

	if files.isdir("bundle_resources") then
		return true
	else
		return false
	end
end

local function load_image(self, image_to_load, texture_id)
	local extra_path = ""

	if is_editor() then
		extra_path = "../../"
	end	
	local dir1 = directories.path_for_file(extra_path .. image_to_load)

	if files.exists(dir1) then
		local f = io.open(dir1, "rb")
		local bytes = f:read("*a")
		local img = image.load(bytes)
		if not img then	return false end

		if self.texture_loaded[texture_id] then
			gui.delete_texture(texture_id)
		end
		
		if gui.new_texture(texture_id, img.width, img.height, img.type, img.buffer) then
			self.texture_loaded[texture_id] = true
			print("Attempting to set texture...")
			gui.set_texture(IMAGE_GUI, texture_id)
		end		
	end

	
		
end

local function load_image_1(self)
	print("Splash portals - loading image 1")
	load_image(self, "splash_portal_1.png", TEXTURE_ID_1)

end

local function load_image_2(self)
	print("Splash portals - loading image 2")
	if self.texture_loaded[TEXTURE_ID_1] then
		self.texture_loaded[TEXTURE_ID_1] = nil
		gui.delete_texture(TEXTURE_ID_1)
	end	
	load_image(self, "splash_portal_2.png", TEXTURE_ID_2)
end

function init(self)
	self.texture_loaded = {}
	IMAGE_GUI = gui.get_node("image")
	--print("Image loader ", msg.url())
	
end

function final(self)
	if self.texture_loaded[TEXTURE_ID_1] then
		gui.delete_texture(TEXTURE_ID_1)
	end
	if self.texture_loaded[TEXTURE_ID_2] then
		gui.delete_texture(TEXTURE_ID_2)
	end	
end

function update(self, dt)
	-- Add update code here
	-- Remove this function if not needed
end

function on_message(self, message_id, message, sender)
	if message_id == hash("load_image_1") then
		load_image_1(self)
	end
	if message_id == hash("load_image_2") then
		load_image_2(self)
	end	
end

function on_input(self, action_id, action)
	-- Add input-handling code here
	-- Remove this function if not needed
end

function on_reload(self)
	-- Add input-handling code here
	-- Remove this function if not needed
end


3 Likes

Yeah, it has to sprites because the modding contains custom images for items. That’s why I’m using imageloader.load() instead of image.load() because I’m trying to set a texture for a sprite.

I’ve also trieda setting the image of a sprite from disk, but that throws an error saying something along he lines of go.set() failed with an error code of -10.

Do you think there is a way to save files to the Defold project during runtime? Because like, when I do os.execute("dir") all the project files show up. Is there a way?

I found out the problem:

I read a different post saying that since you’re changing the texture, the the image in it, the loaded image has to be the same size as the texture. The OG image was 100x100 but the atlas the sprite was using is 128x128 so I changed the image size and it worked fine.

In the future I will just make the image dimensions by a power of 2, just make some of the pixels transparent so it’s compatible.

4 Likes

UDPATE FOR ANYONE STILL FOLLOWING THIS THREAD

Since this post, Defold has been updated so now you can dynamically create atlases. You no longer need to do anything like what was said above.

local texture_name = "my_texture"
--create image resource from raw image data
 local image_resource = imageloader.load{data = image_data} 
--create the texture
--no need to add ".loaded", this is just for me to differenciate between runtime-loaded textures and normal texures
local texture_path = resource.create_texture(texture_name..".loaded.texturec", image_resource.header, image_resource.buffer)

...

Now we will tell it where the animations are by splitting the image. I already have an external json file that I converted to a lua table that dictates the x, y, width, and height of the different pieces of the “spritesheet”.
Note: You don’t need to use any json, this code is pulled out of a project that requires me to use javascript. You can easily make a lua table inside your code that has the same information.

BUT if you’re making something that will be importing images dynamically, you will most likely need to create an external json file to go with the texture image and convert it to a table through json.decode().

baka
(dont judge me i worked on this in like 2021 ok)

    "allocs": {
           {
            "id": "frame0",
            "x": 0,
            "y": 0,
            "width": 128,
            "height": 128
        }, {
            "id": "frame1",
            "x": 129,
            "y": 0,
            "width": 128,
            "height": 128
        }, {
            "id": "frame2",
            "x": 271,
            "y": 14,
            "width": 100,
            "height": 100
        }, {
            "id": "frame3",
            "x": 385,
            "y": 0,
            "width": 128,
            "height": 128
        }
    }

Even though many of these dimensions are 128x128, they don’t have to be! They don’t need to be square nor a factor of 2. I’m just using an example from when I needed to have it like that.

Now, you need to show where the geometries are. You’re basically just telling the code where in the image this single clip is and how big it is. Since we are giving the variables through a table, we can just iterate it:

--dont mind the weird indentations, I pulled the code out of a project
            local geos = {}
            for j, w in ipairs(allocs) do
                table.insert(geos, {
                    vertices = {
                        0, 0,
                        0, w.height,
                         w.width, w.height,
                        w.width, 0
                    },
                    uvs = {
                        w.x, w.y,
                        w.x, w.y + w.height,
                        w.x + w.width, w.y + w.height,
                        w.x + w.width, w.y
                    },
                    indices = {0, 1, 2, 0, 2, 3} -- i have no idea what this does or means lol
                })
            end

Now you add in the animations! According to the API reference for the function we’ll be using, the frame range is non-inclusive of the last number, frame_end, so frame_end - frame_start must equal the amount of frames in the animation.

local anim = {}

--animation of all four frames
table.insert(anim, {
    id = "all",
    width = 128,
    height = 128,
    frame_start = 1,
    frame_end = 5,
    fps = 12
})

--animation of a single frame (the first one)
frame = allocs[1]
table.insert(anim, {
    id = frame.id,
    width = frame.width,
    height = frame.height,
    frame_start = 1,
    frame_end = 2
})

And if you wanted to add every single one independently, you could always iterate it.
Now we can actually create the atlas.

           --again, ".loaded" isn't needed here
            local atlas_path = resource.create_atlas(texture_name..".loaded.texturesetc", {
                texture = texture_path,
                animations = anims,
                geometries = geos
            })

Yay! Now a sprite can take this atlas and play an animation.

go.set("#sprite", "image", atlas_path)
sprite.play_flipbook("#sprite", "all")

When you’re done and want to delete the texture and atlas, make sure to either delete the objects that were using the atlas or change to a different one before deleting.

resource.release(texture_path)
resource.release(atlas_path)

This code had been pulled out and modified from one of my projects (geometry code isn’t mine either, I got it off some other forum post), so there could be mistakes. This is at least the gist of what needs to be done.

6 Likes