Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Monument Valley-style orthogonal projection 3D

edited February 2015 in Suggestions Posts: 2,008

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!


  • Posts: 1,976

    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?

  • Posts: 2,008

    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 ).

  • Posts: 2,008

    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.

  • edited February 2015 Posts: 449

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

  • Posts: 1,976

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

  • Posts: 449

    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.

  • Posts: 2,008

    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?

  • Posts: 449

    yes, kind of, but instead for 3d

  • Posts: 2,008

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

  • edited July 2015 Posts: 449

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

    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:

  • Posts: 2,008

    That's a nice model!

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

  • Posts: 449

    @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))))

  • t1_t1_
    edited December 2016 Posts: 12

    @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 :) 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:)

  • t1_t1_
    edited December 2016 Posts: 12

    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.

  • t1_t1_
    Posts: 12

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

  • Posts: 27

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

  • t1_t1_
    Posts: 12

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

  • Posts: 2,008

    Looking very nice! Good work @Valgo and @t1_

  • t1_t1_
    edited March 26 Posts: 12

    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 :) 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) end return ret end

    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:

  • edited March 29 Posts: 453

    I had thought about this yonks ago in terms of trying to do old fashioned isometric style games but with 3d assets, but never got my head around what I'd have to do to the projection matrix. Didn't know you could ortho with parameters like this...

    Below is an instanced shader example from the forum, but patched to ortho projection. It messes with your head because my brain assumes it's perspective so I think they slope away from each other, but it's all parallel. Very cool. You could build a Fez like thing using this very well as well I think...

    function setup()
        m = mesh()
        m.vertices, m.texCoords = addCube()
        m.shader = shader(vert, frag)
        m.texture = readImage("Planet Cute:Wall Block")
        numInstances = 100
        dummy = m:buffer("dummy") --test vec4 attribute
        transform = m:buffer("transform") --test mat4 attribute
        -- Here we resize the transform buffer to
        -- the number of instances, rather than vertices
        -- note that the buffer can be any size:
        --  if you render N instances with a buffer sized to M
        --  where N > M then the instances will use a divided
        --  version of the buffer
        -- E.g. I render 10 instances with 5 transforms then
        --  instance 1,2 use transform 1
        --  instance 3,4 use transform 2
        --  instance 5,6 use transform 3
        --  ... etc
        -- We have to tell Codea to treat this buffer as an
        -- instanced buffer, that is, it is not per-vertex
        transform.instanced = true
        for i=1,m.size do
            -- Set per vertex
            dummy[i] = vec4(0,0,0,0)
        for i=1,numInstances do
            -- Sets one transform *per instance*
            local x = ((i-1)%10) - 5.5
            local y = math.floor((i-1)/10) - 5.5
            transform[i] = matrix():translate(x * 2, y * 2, 0)
        rot = 0
        pos = vec3(0,0,0)
    function draw()
        background(40, 40, 50)
        camera(30,40,20, 0,0,0, 0,0,1)
        --another angle:
        -- mesh:draw can now take a number of instances
        -- it draws this many, instanced buffers can be
        -- used to differentiate each instance within a
        -- shader
        rot = (rot + 0.2%360)
        --pos = pos + vec3(-0.02,0,0)
    uniform mat4 modelViewProjection;
    attribute mat4 transform;
    attribute vec4 position;
    attribute vec4 color;
    attribute vec4 dummy;
    attribute mediump vec2 texCoord;
    varying lowp vec4 vColor;
    varying mediump vec2 vTexCoord;
    void main()
        vColor = color;
        vTexCoord = texCoord;
        gl_Position = modelViewProjection * (transform * position + dummy); } ]]
    uniform sampler2D texture;
    varying lowp vec4 vColor;
    varying mediump vec2 vTexCoord;
    void main()
        gl_FragColor = texture2D(texture, vTexCoord) * vColor; } ]]
    function addCube()
            local vertices = {
          vec3(-0.5, -0.5,  0.5), -- Left  bottom front
          vec3( 0.5, -0.5,  0.5), -- Right bottom front
          vec3( 0.5,  0.5,  0.5), -- Right top    front
          vec3(-0.5,  0.5,  0.5), -- Left  top    front
          vec3(-0.5, -0.5, -0.5), -- Left  bottom back
          vec3( 0.5, -0.5, -0.5), -- Right bottom back
          vec3( 0.5,  0.5, -0.5), -- Right top    back
          vec3(-0.5,  0.5, -0.5), -- Left  top    back
        -- now construct a cube out of the vertices above
        local cubeverts = {
          -- Front
          vertices[1], vertices[2], vertices[3],
          vertices[1], vertices[3], vertices[4],
          -- Right
          vertices[2], vertices[6], vertices[7],
          vertices[2], vertices[7], vertices[3],
          -- Back
          vertices[6], vertices[5], vertices[8],
          vertices[6], vertices[8], vertices[7],
          -- Left
          vertices[5], vertices[1], vertices[4],
          vertices[5], vertices[4], vertices[8],
          -- Top
          vertices[4], vertices[3], vertices[7],
          vertices[4], vertices[7], vertices[8],
          -- Bottom
          vertices[5], vertices[6], vertices[2],
          vertices[5], vertices[2], vertices[1],
        -- all the unique texture positions needed
        local texvertices = { vec2(0.03,0.24),
                              vec2(0.97,0.69) }
        -- apply the texture coordinates to each triangle
        local cubetexCoords = {
          -- Front
          texvertices[1], texvertices[2], texvertices[4],
          texvertices[1], texvertices[4], texvertices[3],
          -- Right
          texvertices[1], texvertices[2], texvertices[4],
          texvertices[1], texvertices[4], texvertices[3],
          -- Back
          texvertices[1], texvertices[2], texvertices[4],
          texvertices[1], texvertices[4], texvertices[3],
          -- Left
          texvertices[1], texvertices[2], texvertices[4],
          texvertices[1], texvertices[4], texvertices[3],
          -- Top
          texvertices[1], texvertices[2], texvertices[4],
          texvertices[1], texvertices[4], texvertices[3],
          -- Bottom
          texvertices[1], texvertices[2], texvertices[4],
          texvertices[1], texvertices[4], texvertices[3],
        return cubeverts, cubetexCoords
Sign In or Register to comment.