Monument Valley-style orthogonal projection 3D

Like many others I loved Monument Valley, and especially the Escher-esque puzzles that it creates by taking perspective out of the equation.

That got me thinking about how to achieve that projection in Codea (ie objects do not recede with distance, and parallel lines stay parallel).

For the 3D experts on the forum this probably seems obvious, but I couldn’t find any discussion of this on the forum so I thought I’d post.

In the 3D Lab demo that ships with Codea,

  • set the “Angle” parameter to default to 45 degrees:
parameter.number("Angle",-360, 360, 45)
  • add this anywhere in the setup function:
  • and, the key part: comment out the perspective line in the draw function and replace it with this:
    --perspective(FieldOfView, WIDTH/HEIGHT) 
    ortho(-centre.x,centre.x,-centre.y,centre.y,-2000,2000) --orthogonal projection

This is what you get:

orthogonal blocks

Now you just have to come up with the compelling level design, atmospheric graphic style, elliptic story-telling, immersive soundscapes, and Escher-esque puzzles, and you’ve got a certified iOS hit!

I had tried to do this, but it crashed whenever I called ortho(). I didn’t know there were any other parameters. What are the parameters?

From the reference:

ortho( left, right, bottom, top,
near, far )

Sets the projection matrix to the orthographic projection defined by the parameters left, right, bottom, top, near and far. The near and far values specify the closest and farthest distance an object can be without being clipped by the view frustum.

When called with no arguments, sets up the default orthographic projection, equivalent to ortho( 0, WIDTH, 0, HEIGHT, -10, 10 ).

The left, right, top, and bottom parameters can be adjusted to effect the scale of the image.

near and far should be set to how deep your world extends.

there seems to be no such thing as a view frustum… although its stated in the documentation…

@se24vad Things get clipped, but only if they’re really far away.

I have to test this. but when things are right in front (and even go much outside the view) they seem not to be clipped.

Reviving a thread from a few months ago…


For setting the view frustum in orthogonal projection, this works for me:

ortho(0, WIDTH, 0, HEIGHT,-2000,2000) --orthogonal projection

Objects outside of the -2000 to 2000 depth range don’t get drawn.

Is that what you meant?

yes, kind of, but instead for 3d

Does the near and far variable in the perspective command not work? Far works, haven’t experimented with near

I havent experimented as well… and thats why I assumed there was no frustum culling for 3d… but I still could be wrong) -cheers :slight_smile:

Edit: I was working on a obj importer, ages ago^^, and then this question came to me… but I discontinued dev on this, so… now its just nice to know but as I said, I havent tried.

Here is the video of my Project:

That’s a nice model!

Did you see @Ignatz 's .obj importer? I added frame interpolation, for animations:

@yojimbo2000 yes, I actually saw that thread and you both did tremendous work! I played with the idea using your code to extend mine… but for that I need to have frustum culling for the whole 3d scene and other usefull optimizations… maybe I come back to this project one day, because right now I’m just not feeling like it))))

@yojimbo2000 Oh wow this is awesome, thank you so much for sharing! I also loved Monument Valley and have been playing around with doing some MV-esque stuff :slight_smile: Funny I stumbled across this while searching up ways of getting the isometric view. I’m going to take a closer look at remixing the code in the 3D Lab.

Here is just how it looks at the moment, most of the code is thanks to @Valgo :smile:; I’m just remixing to play around:

screenshot of 3D pixelated sphere

That one uses rotate(90,0,-1,1) to get the camera angle but I think there’s still perspective and the camera is a little too high up. Also, the directions are a little mixed up from that; going up on the screen becomes going in the positive X direction.

There’s more of my discussion on that here on doing the shading over time and here on adding the background (kind of funnily, we’re discussing on another forum :grimace:)

I’ve been able to get a test drawing in view with using the camera now. :smiley:

Camera angle of a single column of blocks in sort-of isometric view WIP

I rotated the structure 45 degrees around the Y axis using rotate(45, 0, 1, 0) (which I did try before as well, this gets the basic isometric view but at the moment it is flat).

Then the arguments for ortho() (left, right, bottom, top, near, far) are set specifically for the size of the drawing in this project, I also found that increasing far stopped the drawing from being clipped.

For the camera() part, the first three arguments (EyeX, EyeY, EyeZ) I had to play around with until the drawing showed up. (You can see me setting parameters for them in the pic above.)

I set the centreX, centreY, centreZ arguments to (0,0,0) because that was where everything was in the project. For the last three arguments I left it at the default (0,1,0). (I tried playing around with them in the 3D Lab demo but all I’ve discovered at the moment is that changing upX seems to rotate the view. So I’m not quite sure what it is for yet.)

But now I just need to find a good position for the camera so that the scene appears like it has ‘no perspective’. I’ll probably bringing some more blocks so I can gauge whether it looks like there is perspective.

Thank you again to everyone! :blush:

I would like to share the code if that’s of any interest, I just need permission and just need to give credit :smiley:

And I am explaining a lot of what I found out as I did not know before as well.

@Valgo may I share the code for this here with credit to you?

@t1_ that’s fine, as long as you give credit

@Valgo yep of course I will :smile: thank you!

Looking very nice! Good work @Valgo and @t1_

Thank you for the encouragement yojimbo2000 :blush:

this is an older thing I wrote a while ago… but I’ll put it here anyway

At the moment I made it so you can change the colours for two different block colours using parameters, but it’ll be relatively straightforward to add more colours for blocks. And it picks random colours each time you restart the project too.

Valgo’s code for generating structures is still there (I put it under a tab called SceneDataModel)

Here is a quick pic of a structure I just decided to try out:
Image of structure with columns in blue and magenta colo(u)rs

And here is the link to the code in a pastebin, but if anyone happens to take a peek at it, you’re going to have to forgive me — things are all over the place :joy: I’m still learning :slight_smile: I’d greatly appreciate any tips or ideas anyone may have too.

(I liked being able to separate things into different tabs but wasn’t sure in many cases whether a new class might work better or if just a file would be fine. I can find things when I need to in either case, but it probably doesn’t make much sense to anyone else at the moment which isn’t ideal :sweat_smile:)

Right now I know which part does the gradient (it’s in SceneRenderer) and I thought I made it so that it works like this:

  • For each vertex in the vertices for the cube mesh:
  • Get the distance between that vertex and a reference point, and mix in an amount of black according to that distance (more black if closer, less if further away)

And here was the code that I did:

function SceneRenderer:cubeColor(col, vecX, vecY, vecZ)
    local ret = {}
    vecRefPoint = vec3(-shapeWidth,-shapeWidth,-shapeWidth)
    for vertIdx, vertVec in ipairs(cubeMeshVerts) do
        --Base shading for perspective
        local baseShadeColr = col:mix(color(0, 0, 0, col.a), 1 - math.floor(((vertIdx - 1) % 18) / 6) * 0.15)
        --Shading for gradient
        vecToPoint = vec3((cubeSize*vecX)+vertVec.x,(cubeSize*vecY)+vertVec.y,(cubeSize*vecZ)+vertVec.z)
        distToRefPoint = vecRefPoint:dist(vecToPoint)
        maxDistAcross = vec3(-shapeWidth,-shapeWidth,-shapeWidth):dist(vec3(shapeWidth,shapeWidth,shapeWidth))
        baseShadeColr = baseShadeColr:mix(color(0), distToRefPoint/(maxDistAcross)) 
        table.insert(ret, baseShadeColr)
    return ret

However, when I change the coordinates for vecRefPointit doesn’t seem to change where the lighting is coming from; the darkest part still seems to be at (0,0,0). But maybe I just haven’t tried enough coordinates and enough structures to see yet, so I’ll take a look at a few more.

Since then, I don’t know if I changed the code but most recent pastebin here and since then, I did a random super long explanation of it here :lol: