2d grid distorted when viewed in 3d?

I created an 2d image of a square grid and then to view it in 3d i used it as the floor in the 3d lab project. Unexpectedly (for me) the grid does not look like what i would expect (the lines getting closer together as they go further away) instead the grid is distorted (as if there is a kink along the diagonal of the grid). Am i doing something wrong?

I would add an image to the post but i don’t remember how to do that!?

Post the code, please

@piinthesky Here’s something I have, but I don’t know if it will help or not. Slide your finger around the screen to look in different directions. It’s a 2d grid in 3D space.

displayMode(FULLSCREEN)   

function setup()
    angH,angV=180,0 -- horizontal and verticle angle
end

function draw()
    background(0)
    x=math.cos(math.rad(angV))*math.sin(math.rad(angH))
    y=math.sin(math.rad(angV))
    z=math.cos(math.rad(angV))*math.cos(math.rad(angH)) 
    lookX=1000*x
    lookY=1000*y
    lookZ=1000*z
    perspective()
    camera(0,0,400,lookX,lookY,lookZ,0,1,0)
    stroke(255)
    strokeWidth(2)  
    for z=-1000,1000,20 do  
        line(z,-1000,z,1000)
        line(-1000,z,1000,z)
    end
end

function touched(t)
    if t.state==MOVING then
        angH=angH+t.deltaX/5
        angV=angV-t.deltaY/5
    end
end

Don’t draw the floor the way it is done in the 3D demo. You should use meshes to draw in 3D, not sprites. (If you don’t know meshes, then you have no business drawing in 3D. Learn meshes first!).

function setup()
    local img=MakeGrid(100,100,10) --create grid once only
    floor=MakeFloor(0,-300,800,5000,img) --make a floor mesh
end

function draw()
    background(120)
    perspective()
    camera(0,100,200,0,40,-100)
    -- see how simple it is if you set it up correctly
    --no need to translate or rotate, just draw it
    floor:draw() 
end

--if you know meshes, this is very simple
function MakeFloor(x,z,w,d,img) --x,z are centre of floor, w=width,d=depth
    local m=mesh()
    local x1,x2,z1,z2=x-w/2,x+w/2,z+d/2,z-d/2
    m.vertices={vec3(x1,0,z1),vec3(x2,0,z1),vec3(x2,0,z2),vec3(x2,0,z2),vec3(x1,0,z2),vec3(x1,0,z1)}
    m.texCoords={vec2(0,0),vec2(1,0),vec2(1,1),vec2(1,1),vec2(0,1),vec2(0,0)}
    m.texture=img
    m:setColors(color(255))
    return m
end

--create an image with a grid in it
function MakeGrid(w,h,s) --width, height of image, s=size of each square
    local img=image(w,h)
    setContext(img)
    stroke(0)
    strokeWidth(1)
    for i=0,w,s do line(i,0,i,h) end
    for i=0,h,s do line(0,i,w,i) end
    setContext()
    return img
end

Without seeing code its hard to say what the distortion you’re describing is, but you could experiment with the field of view parameter (the first parameter in the perspective command).

If you have any kind of photography background then a lot of this will be familiar. A relatively wide-angle lens (eg perspective(60)) will have to, by definition, bend straight lines to fit all of the view in, this is true with real-life lenses as it is with virtual ones in the digital world. It’s particularly noticeable towards the corners of the frame, where spherical objects will start to stretch into egg shapes. Calling perspective() without any parameters will default to a 45 degree FoV, regarded as a “natural” FoV (ie close to the human eye, and therefore what we would call in our anthropocentric way, “undistorted”. If bumble-bees had created OpenGL, they might have something to say about that). A more zoom lens (say, perspective(30)) will have a lot less distortion, but also have less depth separation (objects won’t recede as much as they get further away), and of course, less wide of a field of view. Generally, first-person perspective games tend to go for the wide end, around 60 degrees. Although this introduces distortions which can appear unnatural, the wider view is a way to simulate human peripheral vision.

You can have fun recreating Hitchcock’s Vertigo effect, by tracking the camera in at the same time as zooming out.

Thanks for your advice.

I am making an 3D event display for the Antares deep sea neutrino telescope using codea and the @loopspace shape library. It allows to visualise high energy muon particles passing through the telescope emitting cherenkov light which is detected by the photosensors (if interested see antares.in2p3.fr for more details).

The first video below shows the display using a sprite for the reference system grid. You can see the kink in the grid lines i was referring to.

https://youtu.be/JVn6vy_-jLA

In the next video i create the reference system grid by creating a thin 3D block and add the grid texture to it. The grid lines now behave better but obviously i still don’t have it right. Interestingly, the grid lines on the top and underneath of the block are orthogonal! I need to figure out how to wrap the texture correctly around the block.

https://youtu.be/pxbIqAXZ_LA

I also tried @Ignatz code, but for some reason at the moment it yields a grey rectangle when inserted in my code (works fine as standalone code).

edit: fixed my stupid bug in the Ignatz method now it works…

https://youtu.be/ssAuiFRrt7o

Very nice! Let us know if you have any more problems. If it runs slow, you can fake the bubble shapes in 2D.

If you have a trick to make spheres in 2D that correctly handle the shading as they are rotated i would be very interested. Our next telescope has ten times more strings, so the fps will become a big issue.

The fps is affected by the total number of vertices, half of which are never seen (but still require processing), and most of which are not needed anyway as many of the spheres are very small and don’t need high res meshes. So using actual 3D for such small spheres is overkill.

The simplest may be to fake the 3D spheres using a single circular b/w 2D image that looks like a shaded sphere, and colour it using a transparent blend, maybe with a shader for speed.

If that sounds ok, we can mock something up. If you need something more complex, please specify it clearly so we don’t waste each other’s time.

@piinthesky - this code creates and displays a fake 3D ball in any colour, viewed from any angle. You simply translate to the position you want, set the scale to the size you want, and draw it

function setup()
    ball=MakeBall() --create just one ball mesh
    --you can play with colours
    parameter.integer("Red",0,255,255)
    parameter.integer("Green",0,255,255)
    parameter.integer("Blue",0,255,255)
end

function draw()
    background(120)
    perspective()
    camera(0,0,200,0,0,-1) 
    --draw the first ball at 0,0,0
    ball:setColors(color(Red,Green,Blue))
    ball:draw()
    --draw a second smaller ball In a different colour
    pushMatrix()
    translate(0,50,0)
    scale(0.8,0.8)
    ball:setColors(color(255-Red,255-Green,255-Blue))
    ball:draw()
    popMatrix()
end

--only runs once
function MakeBall()
    --build the ball image for this demo
    --you will probably want to import a good image instead
    local r=100
    local base=150
    local spread=0.7
    local img=image(r*2,r*2)
    setContext(img)
    fill(base)
    ellipse(r,r,r)
    local p=vec2(1,1)*(r+r*0.1)
    for i=r*spread,1,-1 do
        c=(255-base)*(1-i/r/spread)^1.2+base
        fill(c)
        --fill(255)
        ellipse(p.x,p.y,i)
    end
    setContext()
    
    --create the mesh
    local m=mesh()
    m:addRect(0,0,r,r)
    m.texture=img
    return m
end

Thanks @Ignatz. To test i introduced your 2D sphere into the 3D lab example. I rotate the plane of the image to always face the camera so that it gives the illusion of being a sphere. This works nicely, but if there is another image behind the sphere a square frame around the sphere is noticable which deletes the images behind.

Interestingly, when i tried to make a movie of the effect using the codea movie tool i am not able to do so, as activating the filming changes the images on the screen and partially fixes the problem!

I was able to film the equivalent effect with the standard 3d lab objects.

https://youtu.be/xDl-7pmm2pI

Note, that for your code above noStroke() should be applied otherwise the gradient coloring does not work correctly.

@piinthesky - ah yes, I should have remembered.

You need to draw the circles from furthest to nearest, which means sorting them before drawing them. This will do it. Assuming you have the circles in a table like this

--each element of the table is a subtable holding the
--circle position plus the size of the circle
circles={
    {vec3(1,1,0),0.6},
    {vec3(20,35,0),0.4}
}

--then if the camera is at position C (a vec3), you can sort it with
table.sort(circles,function(a,b) return a[1]:dist(C)>b[1]:dist(C) end)

The reason you need to sort is that when OpenGL draws your images, and two images overlap, it only shows the front image. However, if the front image has transparent pixels, OpenGL treats them as opaque, and so if the front image is drawn first, and then the back image, OpenGL won’t draw any pixels for the back image if they are behind a transparent pixel from the front image.

The only solution is to draw these images from furthest to nearest, as above.

@piinthesky Your post looked interesting, so I thought I’d see what I could come up with. I tried to keep it simple and still use 3D spheres. This runs at about 16 FPS on my iPad Air. This could probable be speeded up more, but I’m not going to try now. Just thought I’d post it just in case you see anything you could use.

EDIT: Added code to randomly change the color of some of the spheres.

EDIT: Modified the code. It now runs at approx 56 FPS on iPad Air with 320 spheres.

EDIT: Added the white verticle posts, FPS now about 52.

supportedOrientations(LANDSCAPE_ANY)

function setup()
    FPS=60
    parameter.integer("ex",-100,1000,150)
    parameter.integer("ey",-100,1000,30)
    parameter.integer("ez",-100,1000,160)  
    -- creare x,y,z 3d sphere coordinates
    tab={}
    lim=13
    for n=0,lim do
        tab[n]={}
        for m=0,lim do
            x=math.sin(math.pi*m/lim)*math.cos(2*math.pi*n/lim)
            y=math.sin(math.pi*m/lim)*math.sin(2*math.pi*n/lim)
            z=math.cos(math.pi*m/lim)
            tab[n][m]=vec3(x,y,z)
        end
    end    
    -- create sphere triangles using x,y,x coord.
    sph={}
    for n=0,lim-1 do
        for m=0,lim-1 do
            table.insert(sph,tab[n][m])
            table.insert(sph,tab[n][m+1])
            table.insert(sph,tab[n+1][m+1])  
            table.insert(sph,tab[n][m])
            table.insert(sph,tab[n+1][m])
            table.insert(sph,tab[n+1][m+1])
        end
    end    
    sphere=mesh()
    sphere.vertices=sph
    sphere:setColors(255,0,0,255)
    
    sphere1=mesh()
    sphere1.vertices=sph
    sphere1:setColors(255,255,0)
    cnt=10
end

function rnd()
    cnt=cnt+1
    if cnt<5 then
        return
    end
    cnt=0   
    col={} 
    for x=1,4 do
        col[x]={}
        for y=1,30 do
            col[x][y]={}
            for z=1,4 do
                col[x][y][z]={}
                col[x][y][z]=0
                if math.random(100)>80 then
                    col[x][y][z]=1
                end               
            end
        end
    end
end

function draw()  
    rnd()
    background(40, 40, 50)    
    FPS=0.9*FPS+0.1/DeltaTime
    text(FPS//1,WIDTH/2,HEIGHT-10)
    perspective()
    camera(ex,ey,ez,0,0,0)  
    
    -- draw base
    fill(86, 161, 231, 255)
    stroke(255)
    strokeWidth(1)
    pushMatrix()
    rotate(90,1,0,0)
    rect(20,20,110,110)
    popMatrix()  
      
    -- draw lines on base
    pushMatrix()
    rotate(90,1,0,0)
    stroke(0)
    translate(0,0,-1)
    for x=30,120,30 do
        for y=30,120,30 do
            line(x,30,x,120)
            line(30,y,120,y)
        end
    end
    popMatrix()   
    
    -- draw verticle posts
    stroke(255)
    for x=1,4 do
        for y=1,4 do
            pushMatrix()
            translate(x*30,0,y*30)
            line(0,0,0,80)
            popMatrix()
        end
    end
    
    -- draw multiple spheres
    stroke(255)
    for x=1,4 do
        for y=1,20 do
            for z=1,4 do
                pushMatrix()
                translate(x*30,y*4,z*30)
                if col[x][y][z]==1 then
                    sphere1:draw()
                else
                    sphere:draw()
                end
                popMatrix()
            end
        end
    end
end

Thanks @dave1707 that is very nice. You confirm that the real spheres really hurt the fps. I am pretty sure that i will be obliged to adopt the 2D sphere trick suggested by @Ignatz in which the plane of the 2D circle is rotated to face the camera.

@Ignatz,@dave1707, rotating the plane of the circle was easy when the camera moved in the horizontal plane around the 2D sphere. Once the camera moves out of the plane it gets trickier to rotate the 2D sphere to face the camera. If the camera location, the sphere centre and the normal vector of the 2D sphere are known, how do i figure out the euler angles which need to be applied?

@piinthesky - you are starting to need quite a few 3D techniques, that you will find in my “3D in Codea” ebook here

https://www.dropbox.com/sh/mr2yzp07vffskxt/AACqVnmzpAKOkNDWENPmN4psa

Look for Billboards on p.24 (they are 2D objects rotated to face the camera)

I suggest you skim the whole book, it contains everything I learned while programming 3D in Codea

@piinthesky - this code should handle the rotation for you. Rotating toward a point in three dimensions is tricky, but I’ve provided a very efficient little function to do it for you.

EDITED to include more balls and test FPS. I can get over 200 balls before FPS drops below 60, on my Air 2. Code below has 216 balls.

function setup()
    ball=MakeBall()
    camPos=vec3(0,0,200)
    --circles table stores position and size and colour of each ball
    circles={}
    local n=6 --total number of balls is the cube of this figure
    local u=0
    for i=0,n do
        for j=0,n do
            for k=1,n do
                u=u+1
                local c=color(math.random(0,255),math.random(0,255),math.random0,255)
                circles[u]={vec3(i*10-25,j*10,-k*10),0.03+math.random()*0.03,c}
            end
        end
    end
    --camera movement
    camPos=vec3(0,10,100)
    parameter.integer("cx",-200,200,0) --manual camera controls
    parameter.integer("cy",-50,50,10)
    parameter.integer("cz",-200,200,100)  
    FPS=60
end

function draw()
    background(120)
    FPS=0.9*FPS+0.1/DeltaTime
    perspective()
    camPos=vec3(cx,cy,cz)
    camera(cx,cy,cz,0,50,-50)
    table.sort(circles,function(a,b) return a[1]:dist(camPos)>b[1]:dist(camPos) end)
    for i=1,#circles do
        pushMatrix()
        local c=circles[i]
        --translate(c[1]:unpack())
        translate(c[1].x,c[1].y,c[1].z)
        modelMatrix(LookAtMatrix(c[1],camPos))
        scale(c[2],c[2])
        ball:setColors(c[3])
        ball:draw()
        popMatrix()
    end
    --revert to 2D to write on screen
    ortho()
    viewMatrix(matrix())
    fill(255)
    text("FPS = "..math.floor(FPS),50,HEIGHT-50)
end

function LookAtMatrix(source,target,up)
local Z=(source-target):normalize()
up=up or vec3(0,1,0)
local X=(up:cross(Z)):normalize()
local Y=(Z:cross(X)):normalize()
return matrix(X.x,X.y,X.z,0,Y.x,Y.y,Y.z,0,Z.x,Z.y,Z.z,0,source.x,source.y,source.z,1)
end

function MakeBall()
    --build the ball image, you will probably want to import a good image instead
    local r=100
    local base=150
    local spread=0.7
    local img=image(r*2,r*2)
    setContext(img)
    fill(base)
    ellipse(r,r,r)
    local p=vec2(1,1)*(r+r*0.1)
    for i=r*spread,1,-1 do
        c=(255-base)*(1-i/r/spread)^1.2+base
        fill(c)
        --fill(255)
        ellipse(p.x,p.y,i)
    end
    setContext()
    
    --create the mesh
    local m=mesh()
    m:addRect(0,0,r,r)
    m.texture=img
    return m
end


@piinthesky - edited above code to test speed

@ignatz wonderful, you are a scholar and a gentleman!

3D is tough!

@piinthesky - If you need more speed, you can optimise a bit. For example, you only need to sort the circles when they get out of order. Unless you are moving the camera very quickly, you probably only need to do this once or twice a second, so you can use a counter like so

--in setup
timer=0

--in draw
if math.fmod(timer,30)==0 then --twice a second
    --sort
end
timer=timer+1