Creating a large scrollable image

I’m looking to create a large playing area, bigger than the screen, but only want to display a portion of it. The following is my attempt at this, but it appears to be clipping the image to the screen size. Any suggestions as to where I’m going wrong gratefully received. I’ve also added a couple of tests to print sprites, one inside the original screen dimensions, which works, and one outside, which doesn’t

-- BackgroundTest
displayMode(FULLSCREEN)
-- Use this function to perform your initial setup
function setup()
    globalx=0
    scroll=1
    bg=mesh()
    bgw=2048
    bgh=768
    bgimg=image(bgw,bgh)
    bg.texture=bgimg
    block=25
    setContext(bgimg)
    for i=1,15 do 
        sprite("Cargo Bot:Crate Red 2",800+(block*i),200+3*i,block,block)            
    end 
    setContext()

end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    bg:clear()
    background(40, 40, 50)    
    local bgid=bg:addRect(WIDTH/2,HEIGHT/2,1024,768)
    bg:setRectTex(bgid,globalx/2048,0,0.5,1)
    bg:draw()
    globalx = globalx + scroll
    if globalx>1024 or globalx<0 then
        scroll=scroll*-1
        setContext(bgimg)
        sprite("Cargo Bot:Hints Button",200,200,100,100)     
        sprite("Cargo Bot:Hints Button",1400,400,100,100)     
        setContext()
    end
end

You allocate an image 2048x768 pixels. On a retina device this is a 4096x1536 texture. OpenGL on all iOS devices has a maximum texture size of 2048x2048 pixels.

Allocating large textures like this is also likely to cause your GPU to run out of memory (especially when you start adding other stuff). As you are basically telling it to keep all your textures in memory — not just those being displayed on screen.

It looks like your background image could actually be constructed with a mesh, as it’s all rendered with one texture. So you could create large rects in your mesh, each displaying your texture, and position them as you are positioning your sprites. You then render your mesh in the draw call and scroll that. This will use a lot less memory and likely be faster.

I’ve been testing a bit and I’ve noticed that the bgimg created in the setup function already doesn’t contain all the sprites that were drawn. It seems as if clip is still applied after setContext.
I experimented with using clip() or noClip() but to no avail.
Is it possible to draw to an image at coordinates bigger than WIDTH/HEIGTH ?

.@PTN that would depend on which device you are using. A retina iPad has images limited to WIDTH/HEIGHT because 2048 is the maximum dimension any texture can be in iOS OpenGL. A non-retina iPad should have a maximum size of WIDTH*2 — though I can’t recall if we limited them both for consistency.

I use 3000x3000 images. I would like to visualize them ith codea, but i thought it might not be possible to load due to memory limits. I thought i should read parts of the image only, but i don’t know how to do that. Any suggextions?

Unfortunately you would have to break them up into tiles before loading them.

Perhaps we need to look at a native tiled rendering API for these sorts of use cases.

@Simeon thanks for the info.

My plan is to have destructible landscape in a lemmings/worms type fashion. This works using the above and directly manipulating the image using image.set if I constrain it to the size of the screen. However, I want to have a larger area. The tiled approach won’t work here as it won’t be able to track the damage I don’t think

.@West I think @John might have some good suggestions regarding this. He implemented a physics-driven pixel destruction game some time ago (although I recall that he modified Box2D to add per-pixel collision detection). I’ll ask him if he knows how this might be done.

OK, Have implemented a partial solution to give double the screen width by using two meshes. Doesn’t deal with the interface between the two meshes, but this would be taken care of by a check to see if the object being updated to one of the meshes runs past the end. Probably scalable to multiple meshes too.

-- BackgroundTest
displayMode(FULLSCREEN)
-- Use this function to perform your initial setup
function setup()
    touches={}
    globalx=0
    scroll=1
    bg=mesh()
    bg2=mesh()
    bgw=1024
    bgh=768
    bgimg=image(bgw,bgh)
    bg.texture=bgimg
    block=25
    setContext(bgimg)
    background(219, 43, 43, 255)
    for i=1,15 do 
        sprite("Cargo Bot:Crate Red 2",10+(block*i),200+3*i,block,block)            
    end 
    setContext()
    bgimg2=image(bgw,bgh)
    bg2.texture=bgimg2
    block=25
    setContext(bgimg2)
    background(219, 187, 34, 255)
    for i=1,15 do 
        sprite("Cargo Bot:Crate Red 2",10+(block*i),500-3*i,block,block)            
    end 
    setContext()
end

function touched(touch)
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        touches[touch.id] = touch
    end
end
-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    bg:clear()
    bg2:clear()
    background(40, 40, 50)    
    local bgid=bg:addRect(globalx-1024+WIDTH/2,0+HEIGHT/2,1024,HEIGHT)
    bg:setRectTex(bgid,0,0,1,1)
    local bgid=bg2:addRect(globalx+WIDTH/2,0+HEIGHT/2,1024,HEIGHT)
    bg2:setRectTex(bgid,0,0,1,1)
    bg:draw()
    bg2:draw()
    globalx = globalx + scroll
    if globalx>1024 or globalx<0 then
        scroll=scroll*-1
    end
    for k,touch in pairs(touches) do
        if touch.x>globalx then
      --  text("hello in yellow at screen x "..touch.x.." and globalx "..globalx,touch.x,touch.y)
            setContext(bgimg2)
            sprite("Cargo Bot:Crate Blue 3",touch.x-globalx,touch.y)
            setContext()   
        elseif touch.x<=globalx then
      --  text("hello in red at screen x "..touch.x.." and globalx "..globalx,touch.x,touch.y)       
            setContext(bgimg)
            sprite("Cargo Bot:Crate Green 3",1024-globalx+touch.x,touch.y)
            setContext()
        end
    end  
end

Hi @West, You will have to use tiled images, however this is fine because you just have to do some math to track which tile you are manipulating. You could make your tiles as large or as small as you like (sometimes smaller is better because manipulating the pixels requires them to be re-uploaded to the GPU when they get drawn).

I wrote the terrain system for http://mogothemonkey.com/ which has destructible environments and physics. What you need to do is determine the rectangle in world space that needs to be updated (the rectangle will usually be the size of your explosion. You then determine what tiles this rectangle will intersect with (usually a maximum of 4 adjacent tiles). You then translate your explosion rectangle into image space (by subtracting the tile’s position from the rectangle). You then clip the rectangle (by comparing the bounds of the image with the explosion rectangle). You then set the appropriate pixels. This lets explosions span multiple tiles and is actually more efficient.

If you need some more help understanding I can create a diagram or basic code example.

Thanks @John. I understand the concept, it’s just the implementation of the tiles I’m slightly unsure of. I’ll try it with meshes and assigning a separate image to each one later on if I get a chance to do some coding. Thanks again for your help

I ran into this recently as well and found it quite annoying! It means that large sprite sheets, for example, aren’t possible.

My situation was that I wanted to create a cylinder that could roll, so the biggest cylinder that was visible on the screen would need a texture of π times the height of the screen, more than is currently allowed. Fortunately my cylinder was thin so I could double-up width-wise, but it was irritating to have to work around this. I didn’t think of the alternative of having two meshes. This seems a more reliable workaround, but not any more intuitive to implement.

A cylinder with a texture mapped on that you could rotate in 3D by any chance( there was a sphere demo elsewhere on the forum)? I’d be very interested in that but haven’t had the time to sit down and figure it out

@West Probably the sphere demo was mine (there may have been others). I use a cylinder as a “selection wheel” in my User Interface modules. At the moment, I have a number spinner, a font selector, and a generic “list selector” (which the font one is built on top of, the number spinner has a few additional features which make it sensible to be separate).

The code is buried in my library, which is available online.

Cool-I’ll have a look later

I’ve just looked into this and it seems that 4096x4096 is the maximum texture size for iPad 3 (i.e. the new iPad). I had thought it followed the OpenGL ES 2.0 spec of previous devices.

I will up the limit for iPad 3. Though try to be mindful of this as it would be possible to create code that is incompatible with iPad 2.

It does mean you would be able to create images of up to 2*WIDTH on all devices, though.

Thanks again for the suggestions. Here’s the code for anyone who’s interested

-- BackgroundTest
displayMode(FULLSCREEN)
-- Use this function to perform your initial setup
function setup()
    touches={}
    globalx=0
    globaly=0
    scroll=-1
    vscroll=-1
    cratesize=30
    bgw=100
    bgh=100
    tilew=bgw
    tileh=bgh
    bg={}
    bgimg={}
    numMesh=20
    numMeshVert=8
    crater=readImage("Cargo Bot:Star Empty")
    for v=1,numMeshVert do
        bg[v]={}
        bgimg[v]={}
        for i=1,numMesh do
            bg[v][i]=mesh() 
            bgimg[v][i]=image(bgw,bgh)
            bg[v][i].texture=bgimg[v][i]
            setContext(bgimg[v][i])
            background(math.random(255),math.random(255),math.random(255), 255)
            setContext()
        end
    end

end

function touched(touch)
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        touches[touch.id] = touch
    end
end
-- This function gets called once every frame
function draw()
    -- This sets a dark background color 

    
    background(40, 40, 50)    
    for v=1,numMeshVert do
        for i=1,numMesh do
            bg[v][i]:clear()
            --only draw those that appear on the screen will speed it up
            if i*tilew+globalx>0 and i*tilew+globalx<1024+tilew then
                local bgid=bg[v][i]:addRect(globalx+((i-1)*tilew+tilew/2),((v-1)*tileh+tileh/2),tilew,tileh)
                bg[v][i]:setRectTex(bgid,0,0,1,1)
                bg[v][i]:draw()
            end
       --     text(i*tilew,600,600)
        end
    end
    if tilew*numMesh>WIDTH then
        globalx = globalx + scroll
        if globalx>0 or globalx<-(numMesh)*tilew+WIDTH then
            scroll=scroll*-1
        end
    end
    
    for k,touch in pairs(touches) do
        for v=1,numMeshVert do
            for i=1,numMesh do            
                if touch.x<i*tilew+globalx and touch.x>=(i-1)*tilew+globalx and touch.y<v*tileh+globaly and touch.y>=(v-1)*tileh+globaly then
                    -- centre of the mesh
                    setContext(bgimg[v][i])
                    sprite(crater,(-1*(i-1)*tilew)+touch.x-globalx,(-1*(v-1)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()  
                end
                if touch.x>i*tilew+globalx-cratesize/2 and touch.x<i*tilew+globalx and touch.y<v*tileh+globaly and touch.y>=(v-1)*tileh+globaly and i<numMesh then
                    --leaks into mesh to the right
                    setContext(bgimg[v][i+1])
                    sprite(crater,-i*tilew-globalx+touch.x,(-1*(v-1)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()
                end 
                if touch.x>(i-1)*tilew+globalx and touch.x<(i-1)*tilew+globalx+cratesize/2 and touch.y<v*tileh+globaly and touch.y>=(v-1)*tileh+globaly and i>1 then
                    --leak into mesh to the left
                    setContext(bgimg[v][i-1])
                    sprite(crater,-(i-2)*tilew+touch.x-globalx,(-1*(v-1)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()  
                end
                if touch.x<i*tilew+globalx and touch.x>=(i-1)*tilew+globalx and touch.y>(v-1)*tileh+globaly and touch.y<(v-1)*tileh+globaly+cratesize/2 and v>1 then
                    --leak into mesh below
                    setContext(bgimg[v-1][i])
                    sprite(crater,(-1*(i-1)*tilew)+touch.x-globalx,(-1*(v-2)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()  
                end
               if touch.x<i*tilew+globalx and touch.x>=(i-1)*tilew+globalx and touch.y>v*tileh+globaly-cratesize/2 and touch.y<v*tileh+globaly and v<numMeshVert then
                    --leak into mesh above
                    setContext(bgimg[v+1][i])
                    sprite(crater,(-1*(i-1)*tilew)+touch.x-globalx,(-1*(v)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()  
                end  
                if touch.x>i*tilew+globalx-cratesize/2 and touch.x<i*tilew+globalx and touch.y>v*tileh+globaly-cratesize/2 and touch.y<v*tileh+globaly and i<numMesh and v<numMeshVert then
                    --leaks into mesh up and to the right
                    setContext(bgimg[v+1][i+1])
                    sprite(crater,-i*tilew-globalx+touch.x,(-1*(v)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()
                end 
                  if touch.x>i*tilew+globalx-cratesize/2 and touch.x<i*tilew+globalx and touch.y>(v-1)*tileh+globaly and touch.y<(v-1)*tileh+globaly+cratesize/2 and i<numMesh and v>1 then
                    --leaks into mesh below and to the right
                    setContext(bgimg[v-1][i+1])
                    sprite(crater,-i*tilew-globalx+touch.x,(-1*(v-2)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()
                end         
                if touch.x>(i-1)*tilew+globalx and touch.x<(i-1)*tilew+globalx+cratesize/2 and touch.y>v*tileh+globaly-cratesize/2 and touch.y<v*tileh+globaly and i>1 and v<numMeshVert then
                    --leaks into mesh up and to the left
                    setContext(bgimg[v+1][i-1])
                    sprite(crater,-(i-2)*tilew-globalx+touch.x,(-1*(v)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()
                end         
                    if touch.x>(i-1)*tilew+globalx and touch.x<(i-1)*tilew+globalx+cratesize/2 and touch.y>(v-1)*tileh+globaly and touch.y<(v-1)*tileh+globaly+cratesize/2 and i>1 and v>1 then
                    --leaks into mesh down and to the left
                    setContext(bgimg[v-1][i-1])
                    sprite(crater,-(i-2)*tilew-globalx+touch.x,(-1*(v-2)*tileh)+touch.y-globaly,cratesize,cratesize)
                    setContext()
                end         
            end
       end 
    end  
end

May I ask for clarification about what limitations apply in what contexts?

On an iPad 2, I can create images of any size but sprite renders them as opaque black if they have a dimension greater than 4096. For example:


function setup()
    local w = 10; h = 4096 -- Change h to 4095 for different behaviour
    imgRed = image(w, h)
    print ("Red: "..imgRed.width..", "..imgRed.height)
    imgBlue = image(w, h + 1)
    print ("Blue: "..imgBlue.width..", "..imgBlue.height)
    for j = 1, h do
        for i = 1, w do
            imgRed:set(i, j, color(255, 0, 0))
            imgBlue:set(i, j, color(0, 0, 255))
        end
    end
    print("Red pixel:\
", imgRed:get(w, h))
    print("Blue pixel:\
", imgBlue:get(w, h))
    spriteMode(CENTER)
end

function draw()
    background(127)
    sprite(imgRed, WIDTH/3, HEIGHT/2)
    sprite(imgBlue, WIDTH * 2/3, HEIGHT/2)
end

draws the vertical red bar to the left but a black (not blue) one to the right (unless h + 1 is reduced to 4096).

Also, a similar example with mesh textures:


function setup()
    local w = 16; h = 4096 -- Change h to 4095 for different behaviour
    imgRed = image(w, h)
    print ("Red: "..imgRed.width..", "..imgRed.height)
    imgBlue = image(w, h + 1)
    print ("Blue: "..imgBlue.width..", "..imgBlue.height)
    for j = 1, h do
        for i = 1, w do
            imgRed:set(i, j, color(j % 256, 0, 0))
            imgBlue:set(i, j, color(0, 0, j % 256))
        end
    end
    mRed = mesh()
    mRed.texture = imgRed
    local idx = mRed:addRect(WIDTH/3, HEIGHT/3, 100, 100)
    mRed:setRectTex(idx, 0, 0, 1, 1)
    mBlue = mesh()
    mBlue.texture = imgBlue
    local idx = mBlue:addRect(WIDTH * 2/3, HEIGHT * 2/3, 100, 100)
    mBlue:setRectTex(idx, 0, 0, 1, 1)
end

function draw()
    background(127)
    mRed:draw()
    mBlue:draw()
end

Hey, I did something similar with a little scroll shooter:
http://twolivesleft.com/Codea/Talk/discussion/1167/speed-shooter-earth

I’m trying to do something similar at the moment and have gotten a bit stuck. I’m trying to draw some stuff into a 1024x1024 image (so, 2048x2048 on retina), but it seems to be getting clipped at the screen size - 1024x768. Any thoughts as to why this would be?