1. Anders Ruud
  2. love

Issues

Issue #539 invalid

Normalized texture coordinates (or resizing textures)

Anonymous created an issue

It is a common thing indeed to refer to textures in their absolute pixel sizes, but there should be also normalized coordinates available.

This is especially helpful when giving users the ability to mod your game, without forcing them to make images of certain dimensions so you could put them in the texture atlas.

Currently, the only way to support any arbitrary size is to use a framebuffer (canvas) and scale the graphics before you render the textures, then construct a texture atlas from the buffer.

Comments (14)

  1. Chananya Freiman

    ...I wrote a very long and descriptive comment on why this is needed, but it got deleted before I posted it, so I'll try to make it short and to the point this time.

    The main point is this: decouple texture sizes and geometry sizes, since they are not related.

    The concept of "sprites" is rectangle geometries of any size that reference textures of any size, and I don't see why LOVE sprites should work any different.

    Quads are supposed to solve this, but use pixel coordinates themselves without any option of using normalized coordinates.

    Currently the only option to render sprites of any size in LOVE is by scaling them in every graphics.drawq call, relative to the size of the texture in use. This makes handling tile maps and texture atlases a pain, and includes a lot of arbitrary numbers all over the code.

    If you decouple textures and geometries, it allows you to use -world- coordinates (which you decide what they mean) for your geometries. For example, using 1x1 tiles is very easy, removes a whole lot of calculations from drawing and physics, and makes the code neater.

    Here's an example of pseudo code to draw a tile map tile by tile assuming you have the following information, using what LOVE gives us now: Every texture is 64x64 pixels, and every tile should be 1x1

    love.graphics.scale(...) -- The actual world size you want
    
    loop over rows
        loop over lines
            -- Note how coordinates can be simple (x, y) because I chose 1x1 world units
            -- And that can only be done by scaling the drawing down by the texture size
            love.graphics.drawq(image, quads[y][x], x, y, 0, 1/64, 1/64)
        end
    end
    

    Edit

    I also wanted to add that this relates strongly to how easy it is to integrate your world with Box2D too. When you can use your own world units, it is much easier to sync with Box2D, unlike when you use pixels, which is the reason you keep seeing variables like "PIXELS_PER_METER" in every 2D game example using Box2D, stuck in all the calculations everywhere in the game.

  2. Chananya Freiman

    Yes, a sprite batch does what the code above does, but it doesn't remove the need to do the texture-size based scaling.

    Or in other words, my issue here isn't about code efficiency, it's about code logic and readability. It makes no sense to have calculations all over the code (rendering, logic, physics, etc.) because geometries are effected by textures.

  3. hahawoo

    I'm probably misunderstanding this, but what about using a sprite class which handles scaling and positioning?

    For example, making a new sprite with a width of 1 unit and a height of 2 units, regardless of the actual dimensions of the image:

    test = sprite.new('image.png', 1, 2)
    

    And then drawing it at 3 units on the x-axis and 2 units on the y-axis:

    test:draw(3, 2)
    

    Here's an example class that could do this:

    pixels_per_unit = 16
    
    sprite = {}
    
    function sprite.new(image, width, height)
        local self = setmetatable({}, {__index = sprite})
    
        self.image = love.graphics.newImage(image)
        self.scale_x = width / self.image:getWidth() * pixels_per_unit
        self.scale_y = height / self.image:getHeight() * pixels_per_unit
    
        return self
    end
    
    function sprite:draw(x, y)
        love.graphics.draw(self.image, x * pixels_per_unit, y * pixels_per_unit, 0, self.scale_x, self.scale_y)
    end
    
  4. Chananya Freiman

    That's exactly what I was talking about when I said it shouldn't work like it. In that small example, pixels_per_unit might look harmless, but if you look at it in the context of the game, it is everywhere in the game (like I previously wrote, all of these calculations can be removed with a single scale call before drawing).

    In addition, you still need to do all the texture-based scaling, so it didn't actually help with anything, but rather made a bigger mess.

    Geometry isn't related in any way to textures in OpenGL, and since LOVE is using OpenGL, I can't understand how support for this notion wasn't added.

  5. Matthias Richter

    I totally forgot to reply to your second post (sorry):

    I don't know if such a change will be done. Löve always tries (not always successful though) to provide easy interfaces instead of feature rich ones.

    IMO, the simple love.graphics.draw(img, x,y) is preferable to a possbile love.graphics.draw(img, geometry, x, y), because it requires one less object to keep track of - mentally.

    However, since there already is a love.graphics.drawq and there has been talk about support for textured polygons, you shouldn't write this one off yet ;)

    I am not sure if I understand your problem correctly, but I see two possible workarounds. Both are untested.

    The first creates a closure around love.graphics.draw[q]:

    do
        local draw = love.graphics.draw
        function love.graphics.draw(drawable, x,y, angle, sx,sy, ...)
            if drawable.getWidth then
                -- this is an image: scale to unit width/height
                local norm = math.max(drawable:getWidth(), drawable:getHeight())
                angle = angle or 0
                sx, sy = (sx or 1) * norm, (sy or sx or 1) * norm
            end
            return draw(drawable, x,y, angle, sx,sy, ...)
        end
        -- similar for love.graphics.drawq
    end
    
    
    -- to draw
    love.graphics.scale(world_size)
    for y, row in ipairs(grid) do
        for x, cell in ipairs(row) do
            love.graphics.draw(cell.img, x, y)
        end
    end
    

    ... and the second (ab)uses quads:

    -- creates a 1x1 quad with texture coordinates (0,0), (0,1), (1,1), (0,1)
    quad = love.graphics.newQuad(0,0, 1,1, 1,1)
    
    -- to draw
    love.graphics.scale(world_size)
    for y, row in ipairs(grid) do
        for x, cell in ipairs(row) do
            love.graphics.drawq(cell.img, quad, x, y)
        end
    end
    

    Using a (uniform, i.e. all sprites are the same size) texture atlas should also be possible by setting the reference width and height to cols, rows instead of 1, 1.

    Does that work or am I missing something?

  6. Chananya Freiman
    quad = love.graphics.newQuad(0,0, 1,1, 1,1)
    

    That only grabs 1 pixel, that's the root of the issue.

    I don't really see normalized texture coordinates as an "added feature", because it is the default behavior of OpenGL. I find it weird that we are only given access to pixel-sized coordinates, which Löve then goes on to scale exactly like you are forced to do now in the Lua code.

    The first example can make the code a little neater, but stops you from using sprite batches, which are the only half-efficient way to render tile maps right now (I could upload my really efficient way to render uniform tile maps in C, but I don't really see the developers of Löve being active in this issue tracker anyway).

    As to love.graphics.draw[q], the interface needn't change. If the dimensions are given in the parameter list, use them, if not, use the texture.

    The only place where this really does change the implementation is love.imageData, specifically the paste() method, since it would require scaling of the image data being pasted (either on the CPU or with a framebuffer).

    I am guessing this looks like a silly convention thing to nearly everyone reading this, but from my own experience with 2D games, the benefits of using your own coordinate system instead of pixels are huge (and adding Box2D on top of it makes the benefits even larger).

  7. Matthias Richter

    That only grabs 1 pixel.

    No, it doesn't. It "creates a 1x1 quad with texture coordinates (0,0), (0,1), (1,1), (0,1)". The key is the reference width of 1 by 1 pixels. See the attached example.

    I don't really see normalized texture coordinates as an "added feature", because it is the default behavior of OpenGL. I find it weird that we are only given access to pixel-sized coordinates, which Löve then goes on to scale exactly like you are forced to do now in the Lua code.

    Well, LÖVE is not OpenGL. It is intended to be a fast and easy framework for creating games. OpenGL on the other hand is sort of a smallest common denominator the graphic cards vendors could agree on and is as such not really suited for either a first dive into game programming or fast prototyping (imo LÖVE excels at this).

    Coming from OpenGL, I also found the coordinate system a bit weird at first, but once I accepted it as how things work in LÖVE it doesn't really matter anymore. Or to put it bluntly: Yes, you do have to pull some stunts to do things the OpenGL way, but doing it the LÖVE way is really easy.

    The only place where this really does change the implementation is love.imageData, specifically the paste() method, since it would require scaling of the image data being pasted (either on the CPU or with a framebuffer).

    The love.image module is just for decoding and managing pixel data. It has nothing to do with graphics, so I don't see how a new sprite object would factor into this.

    I could upload my really efficient way to render uniform tile maps in C, but I don't really see the developers of Löve being active in this issue tracker anyway.

    Well, I sometimes am ;) So if your code is generic enough and fits into the current framework, please do show it!

  8. Chananya Freiman

    Why does that work? the documentation says it is the size of the image, so 1x1 should grab one pixel, the same way my 64x64 grabs 64x64 pixels... This does solve my problems, though, so thanks.

    About love.image - I would want to dynamically create a texture atlas from a list of images, some of them might come from arbitrary people, and I don't like size restrictions. I didn't see any way to copy a texture of any NxM size to a sized block on an image, beside using a framebuffer. This is usually done on library code level in most libraries that I have seen.

    As to my tile map renderer, I am not sure how well it fits actually. It requires shaders, so it might conflict with pixel effects. It requires both a vertex and fragment shaders, texture arrays, texture buffers, and instanced rendering. All of these have their own fallbacks, of course, such as when none of them exist on the hardware, you basically get the current sprite batch. All of these features are core since OpenGL 3, and are existing as extensions for literally years by now. What it offers is incredibly fast rendering, with nearly zero memory and bandwidth. If it looks like it can be combined with pixel effects (which seem to be the only thing that might conflict), I'd be happy to put in more details.

    I didn't realize you are a developer either, sorry.

    Edit Ok I understood how quads work. The documentation should be clearer on how they work (by being ratios (the OpenGL way) rather than absolute values (some 2D game engines' way)).

  9. Matthias Richter

    This does solve my problems

    Can this issue be closed then?

    Ok I understood how quads work. The documentation should be clearer on how they work (by being ratios (the OpenGL way) rather than absolute values (some 2D game engines' way)).

    Actually, the documentation talks about "reference width" and "reference height" and also notes that

    The object is first scaled to dimensions sw × sh. The Quad then describes the rectangular area of dimensions width × height whose upper left corner is at position (x, y) inside the scaled object.

    I agree that that is "a bit" cryptic though.

    I didn't see any way to copy a texture of any NxM size to a sized block on an image

    You already found out how to do this: ImageData:paste(). If you want to rescale while pasting, use your favorite sampling method in ImageData:mapPixel(). Anyhow, such questions are better asked on IRC or the forums.

  10. Log in to comment